1. 程式人生 > >《iOS Drawing Practical UIKit Solutions》讀書筆記(四) —— Path Basics

《iOS Drawing Practical UIKit Solutions》讀書筆記(四) —— Path Basics

貝瑟爾曲線

貝瑟爾曲線是在繪製路徑時,常用的方式。通過貝瑟爾曲線,我們可以繪製常規的矩形,橢圓,或弧線。同時,通過二次,三次貝瑟爾曲線,我們還可以繪製更加複雜的曲線。現在,我們就一起來看一下,貝爾瑟爾曲線在iOS繪圖中的應用。

UIBezierPath

UIBezierPath是iOS中提供的貝瑟爾曲線類,它提供了簡便的方法呼叫,來建立矩形、橢圓、圓角矩形以及弧線。

Rectangles

+(instancetype)bezierPathWithRect:(CGRect)rect;

Ovals and circles

+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;

Rounded rectangles

+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; // rounds all corners with the same horizontal and vertical radius

Corner-controlled rounded rectangles

+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners
cornerRadii:(CGSize)cornerRadii;

Arcs

+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;

基礎的繪製例項

UIBezier曲線有如下特性:

  1. 曲線可以通過appendPath函式動態增長
  2. 曲線不必是連續的, 可以分段,最終統一繪製一個貝瑟爾曲線(見下面的例子)

NOTE: 當我們繪製的貝瑟爾曲線太過複雜時,往往會遭遇效能問題。這常見於使用者塗鴉類的應用。對於我們日常的使用,一般是沒有問題的。

這裡寫圖片描述

#pragma mark - General Geometry
CGPoint RectGetCenter(CGRect rect)
{
    return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
}

CGFloat PointDistanceFromPoint(CGPoint p1, CGPoint p2)
{
    CGFloat dx = p2.x - p1.x;
    CGFloat dy = p2.y - p1.y;

    return sqrt(dx*dx + dy*dy);
}

// Radians from degrees
CGFloat RadiansFromDegrees(CGFloat degrees)
{
    return degrees * M_PI / 180.0f;
}

CGRect RectAroundCenter(CGPoint center, CGSize size)
{
    CGFloat halfWidth = size.width / 2.0f;
    CGFloat halfHeight = size.height / 2.0f;

    return CGRectMake(center.x - halfWidth, center.y - halfHeight, size.width, size.height);
}

#pragma mark - drawRect
- (void)drawRect:(CGRect)rect {
    CGRect fullRect = (CGRect){.size = self.frame.size};
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];

    // 畫臉
    CGRect inset = CGRectInset(fullRect, 32, 32);
    UIBezierPath *faceOutline = [UIBezierPath bezierPathWithOvalInRect:inset];
    [bezierPath appendPath:faceOutline];  // 將曲線新增到目的曲線

    // 繼續畫眼睛,嘴巴
    CGRect insetAgain = CGRectInset(inset, 64, 64);

    //計算半徑
    CGPoint referencePoint = CGPointMake(CGRectGetMinX(insetAgain), CGRectGetMaxY(insetAgain));
    CGPoint center = RectGetCenter(inset);
    CGFloat radius = PointDistanceFromPoint(center, referencePoint);

    // 新增笑臉
    UIBezierPath *smile = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:RadiansFromDegrees(40) endAngle:RadiansFromDegrees(140) clockwise:YES];
    [bezierPath appendPath:smile];

    // 左眼
    CGPoint p1 = CGPointMake(CGRectGetMinX(insetAgain), CGRectGetMinX(insetAgain));
    CGRect eyeRect1 = RectAroundCenter(p1, CGSizeMake(20, 20));
    UIBezierPath *eye1 = [UIBezierPath bezierPathWithRect:eyeRect1];
    [bezierPath appendPath:eye1];


    // 右眼
    CGPoint p2 = CGPointMake(CGRectGetMaxX(insetAgain), CGRectGetMinX(insetAgain));
    CGRect eyeRect2 = RectAroundCenter(p2, CGSizeMake(20, 20));
    UIBezierPath *eye2 = [UIBezierPath bezierPathWithRect:eyeRect2];
    [bezierPath appendPath:eye2];

    bezierPath.lineWidth = 3;
    [bezierPath stroke];

}

自定義繪圖

當系統自帶的畫圓,矩形等函式不能滿足我們時,我們可以通過如下函式來自定義繪圖

// 指定曲線起始點
- (void)moveToPoint:(CGPoint)point;
// 新增到point的直線
- (void)addLineToPoint:(CGPoint)point;
// 三次貝瑟爾曲線
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
// 二次貝瑟爾曲線
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
// 弧線
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise

例如,繪製五邊形:

這裡寫圖片描述

    UIColor *color = [UIColor greenColor];
    [color set];

    UIBezierPath *path = [UIBezierPath bezierPath];
    path.lineWidth = 4;
    path.lineJoinStyle = kCGLineCapRound; // 線終點處理
    path.lineCapStyle = kCGLineCapRound;  // 線拐角處理

    // 設定起始點
    [path moveToPoint:CGPointMake(150, 50)];
    [path addLineToPoint:CGPointMake(100, 100)];
    [path addLineToPoint:CGPointMake(110, 190)];
    [path addLineToPoint:CGPointMake(190, 190)];
    [path addLineToPoint:CGPointMake(200, 100)];
    [path closePath]; // 最後一條線,通過閉合所有的點來實現

    [path stroke];

繪製貝瑟爾曲線

一般的,繪製貝瑟爾曲線的目的有兩種:

  1. 將曲線繪製到Context中,並用fill或stroke方法來填充曲線或僅描邊。當使用stroke方法時,我們應該指定曲線的lineWidth屬性。繪製流程一般會像下面這樣:
myPath.lineWidth = 4.0f;
[[UIColor redColor] setStroke];
[[UIColor greenColor] setFill];
[myPath fill];
[myPath stroke];
  1. 結合CAShapeLayer,建立異形的view layer

有用的分類

下面是兩個可用於UIBezierPath的分類,可以簡化fill於stroke的設定:

- (void) stroke: (CGFloat) width color: (UIColor *) color
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);

    PushDraw(^{
        if (color) [color setStroke];
        CGFloat holdWidth = self.lineWidth;
        if (width > 0)
            self.lineWidth = width;
        [self stroke];
        self.lineWidth = holdWidth;
    });
}

- (void) fill: (UIColor *) fillColor
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);

    PushDraw(^{
        if (fillColor)
            [fillColor set];
        [self fill];
    });
}

typedef void (^DrawingBlock)(CGRect bounds);
typedef void (^DrawingStateBlock)();
void PushDraw(DrawingStateBlock block);
void PushLayerDraw(DrawingStateBlock block);

void PushDraw(DrawingStateBlock block)
{
    if (!block) return; // nothing to do

    CGContextRef context = UIGraphicsGetCurrentContext();
    if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);

    CGContextSaveGState(context);
    block();
    CGContextRestoreGState(context);
}

// Improve performance by pre-clipping context
// before beginning layer drawing
void PushLayerDraw(DrawingStateBlock block)
{
    if (!block) return; // nothing to do

    CGContextRef context = UIGraphicsGetCurrentContext();
    if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);

    CGContextBeginTransparencyLayer(context, NULL);
    block();
    CGContextEndTransparencyLayer(context);
}

奇偶填充

當我們用fill填充時,預設曲線內部會被全部填充。
但是,path的屬性:

@property(nonatomic) BOOL usesEvenOddFillRule; // Default is NO. When YES, the even-odd fill rule is used for drawing, clipping, and hit testing.

會將曲線圖像分割成奇偶區域,奇填偶數不填:

這裡寫圖片描述

Path Bounds and Centers

  • 用UIBezierPath的bound屬性,可以獲得包裹所有曲線點的最小矩形
  • 用CoreGraphics方法CGPathGetPathBoundingBox可以獲得更精確的矩形

下面是獲取bound center的工具方法:

#pragma mark - Bounds
CGRect PathBoundingBox(UIBezierPath *path)
{
    return CGPathGetPathBoundingBox(path.CGPath);
}

CGRect PathBoundingBoxWithLineWidth(UIBezierPath *path)
{
    CGRect bounds = PathBoundingBox(path);
    return CGRectInset(bounds, -path.lineWidth / 2.0f, -path.lineWidth / 2.0f);
}

CGPoint PathBoundingCenter(UIBezierPath *path)
{
    return RectGetCenter(PathBoundingBox(path));
}

CGPoint PathCenter(UIBezierPath *path)
{
    return RectGetCenter(path.bounds);
}

Transforming Paths

UIBezierPath 提供了方法applyTransform來讓path調轉:

[path applyTransform:CGAffineTransformMakeRotation(M_PI/9)];

左側是我想要的結果,而右側則是實際的結果:

這裡寫圖片描述

這是因為我們進行仿射變換的錨點預設在座標的原點,而不是path的center。為了讓變換按照我們自己的想法進行,我們必須將變換以path的center為基準:

void ApplyCenteredPathTransform(UIBezierPath *path, CGAffineTransform transform)
{
    CGPoint center = PathBoundingCenter(path);
    CGAffineTransform t = CGAffineTransformIdentity;
    t = CGAffineTransformTranslate(t, center.x, center.y);
    t = CGAffineTransformConcat(transform, t);
    t = CGAffineTransformTranslate(t, -center.x, -center.y);
    [path applyTransform:t];
}

UIBezierPath *PathByApplyingTransform(UIBezierPath *path, CGAffineTransform transform)
{
    UIBezierPath *copy = [path copy];
    ApplyCenteredPathTransform(copy, transform);
    return copy;
}

void RotatePath(UIBezierPath *path, CGFloat theta)
{
    CGAffineTransform t = CGAffineTransformMakeRotation(theta);
    ApplyCenteredPathTransform(path, t);
}

void ScalePath(UIBezierPath *path, CGFloat sx, CGFloat sy)
{
    CGAffineTransform t = CGAffineTransformMakeScale(sx, sy);
    ApplyCenteredPathTransform(path, t);
}

void OffsetPath(UIBezierPath *path, CGSize offset)
{
    CGAffineTransform t = CGAffineTransformMakeTranslation(offset.width, offset.height);
    ApplyCenteredPathTransform(path, t);
}

void MovePathToPoint(UIBezierPath *path, CGPoint destPoint)
{
    CGRect bounds = PathBoundingBox(path);
    CGPoint p1 = bounds.origin;
    CGPoint p2 = destPoint;
    CGSize vector = CGSizeMake(p2.x - p1.x, p2.y - p1.y);
    OffsetPath(path, vector);
}

void MovePathCenterToPoint(UIBezierPath *path, CGPoint destPoint)
{
    CGRect bounds = PathBoundingBox(path);
    CGPoint p1 = bounds.origin;
    CGPoint p2 = destPoint;
    CGSize vector = CGSizeMake(p2.x - p1.x, p2.y - p1.y);
    vector.width -= bounds.size.width / 2.0f;
    vector.height -= bounds.size.height / 2.0f;
    OffsetPath(path, vector);
}

void MirrorPathHorizontally(UIBezierPath *path)
{
    CGAffineTransform t = CGAffineTransformMakeScale(-1, 1);
    ApplyCenteredPathTransform(path, t);
}

void MirrorPathVertically(UIBezierPath *path)
{
    CGAffineTransform t = CGAffineTransformMakeScale(1, -1);
    ApplyCenteredPathTransform(path, t);
}

void FitPathToRect(UIBezierPath *path, CGRect destRect)
{
    CGRect bounds = PathBoundingBox(path);
    CGRect fitRect = RectByFittingRect(bounds, destRect);
    CGFloat scale = AspectScaleFit(bounds.size, destRect);

    CGPoint newCenter = RectGetCenter(fitRect);
    MovePathCenterToPoint(path, newCenter);
    ScalePath(path, scale, scale);
}

void AdjustPathToRect(UIBezierPath *path, CGRect destRect)
{
    CGRect bounds = PathBoundingBox(path);
    CGFloat scaleX = destRect.size.width / bounds.size.width;
    CGFloat scaleY = destRect.size.height / bounds.size.height;

    CGPoint newCenter = RectGetCenter(destRect);
    MovePathCenterToPoint(path, newCenter);
    ScalePath(path, scaleX, scaleY);
}

虛線段

UIBezier曲線設定虛線段很簡單,只需要呼叫函式

- (void)setLineDash:(nullable const CGFloat *)pattern count:(NSInteger)count phase:(CGFloat)phase;

引數:    pattern->C型別CGFlot陣列,表明連續的線段點數和空白線段點數。

      count->pattern中資料個數

      phase-> 起始位置(我們可以通過設定phase,來顯示一個流動的虛線動畫)

例子

CGFloat dashes[] = {6, 2};
[path setLineDash:dashes count:2 phase:0];

這裡寫圖片描述
這裡寫圖片描述

繪製多邊形

下面是一個繪製等邊多邊形的功能函式:

UIBezierPath *BezierPolygon(NSUInteger numberOfSides)
{
    if (numberOfSides < 3)
    {
        NSLog(@"Error: Please supply at least 3 sides");
        return nil;
    }

    CGRect destinationRect = CGRectMake(0, 0, 1, 1);

    UIBezierPath *path = [UIBezierPath bezierPath];
    CGPoint center = RectGetCenter(destinationRect);
    CGFloat r = 0.5f; // radius

    BOOL firstPoint = YES;
    for (int i = 0; i < (numberOfSides - 1); i++)
    {
        CGFloat theta = M_PI + i * TWO_PI / numberOfSides;
        CGFloat dTheta = TWO_PI / numberOfSides;

        CGPoint p;
        if (firstPoint)
        {
            p.x = center.x + r * sin(theta);
            p.y = center.y + r * cos(theta);
            [path moveToPoint:p];
            firstPoint = NO;
        }

        p.x = center.x + r * sin(theta + dTheta);
        p.y = center.y + r * cos(theta + dTheta);
        [path addLineToPoint:p];
    }

    [path closePath];

    return path;
}

Line Joins and Caps

對於線段的連線點(Join),Quartz給出了3中方式,分別是:

kCGLineJoinMiter
kCGLineJoinRound
kCGLineJoinBevel

這裡寫圖片描述

對於線段的終點,也給出了三種方式,分別是:

kCGLineCapButt
kCGLineCapSquare
kCGLineCapRound

這裡寫圖片描述

Miter Limits

Miter limits:
設定最大斜接長度,斜接長度指的是在兩條線交匯處內角和外角之間的距離。超過最大傾斜長度的,連線處將以 lineJoin 為 bevel 來顯示。

這裡寫圖片描述