1 前言
在這個大數據的時代,很多信息只有通過圖形才能更好的展示給用戶。例如:房屋的歷史價格、基金股票的歷史增長、數據佔比分析圖等。如何做圖形?需要用到什麼知識?本文將從 建模、顯示 兩方面來展開介紹。
2 建模
建模是一切圖形的基礎,其他內容的前提,要用代碼展示一個圖形,首先要有它的幾何模型表達。目前在客戶端二維圖形建模上,Bézier curve(貝塞爾曲線)可以稱爲 經典 和 主流 並重的數學曲線。
對於貝塞爾曲線來說,最重要的是 起始點、終止點(也稱錨點)、控制點。控制點決定了一條路徑的彎曲軌跡,根據控制點的個數,貝塞爾曲線被分爲:一階貝塞爾曲線(0個控制點)、二階貝塞爾曲線(1個控制點)、三階貝塞爾曲線(2個控制點)、N階貝塞爾曲線(n - 1個控制點)。
2.1 貝塞爾曲線原理
以二階貝塞爾曲線爲例 解釋說明:
起始點:P0 ; 控制點:P1 ; 終止點:P2
- 連接P0P1線 和 P1P2線。
- 在P0P1線上找到點A,在P1P2線上找到點B,使得 P0A/AP1 = P1B/BP2
- 連接AB,在AB上找到點X,X點滿足:AX/XB = P0A/AP1 = P1B/BP2
- 找出所有滿足公式:AX/XB = P0A/AP1 = P1B/BP2 的X點。(從P0 到 P2的紅色曲線點爲所有X點的連線)這條由所有X點組成的連線 即爲 貝塞爾曲線。
二階貝塞爾曲線 起始點:P0 ; 控制點:P1 ; 終止點:P2
三階貝塞爾曲線 起始點:P0 ; 控制點:P1、P2; 終止點:P3
四階貝塞爾曲線 起始點:P0 ; 控制點:P1、P2、P3 ; 終止點:P4
2.2 UIBezierPath類
系統給我們提供了一個叫做UIBezierPath類,用它可以畫簡單的圓形,橢圓,矩形,圓角矩形,也可以通過添加點去生成任意的圖形,還可以簡單的創建一條二階貝塞爾曲線和三階貝塞爾曲線。我們來了解一下它的常用方法:
2.2.1 初始化方法
// 創建UIBezierPath對象
+ (instancetype)bezierPath;
// 創建在rect內的矩形
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 設定特定的角爲圓角的矩形,corners:指定的角爲圓角,其他角不變,cornerRadii:圓角的大小
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 創建圓弧
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 通過已有路徑創建路徑
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
// 創建三次貝塞爾曲線 endPoint:終點 controlPoint1:控制點1 controlPoint2:控制點2
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
- // 創建二次貝塞爾曲線 endPoint:終點 controlPoint:控制點
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
2.2.2 使用方法
// 移動到某一點
- (void)moveToPoint:(CGPoint)point;
// 繪製一條線
- (void)addLineToPoint:(CGPoint)point;
// 閉合路徑,即在終點和起點連一根線
- (void)closePath;
// 清空路徑
- (void)removeAllPoints;
// 填充
- (void)fill;
// 描邊,路徑創建需要描邊才能顯示出來
- (void)stroke;
2.2.3 常用屬性
// 將UIBezierPath類轉換成CGPath,類似於UIColor的CGColor
@property(nonatomic) CGPathRef CGPath;
// path線的寬度
@property(nonatomic) CGFloat lineWidth;
// path端點樣式
@property(nonatomic) CGLineCap lineCapStyle;
// 拐角樣式
@property(nonatomic) CGLineJoin lineJoinStyle;
2.2.4 舉個栗子🌰
先看效果👇:
代碼如下:
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] set];
// 右邊第一個圖
UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(30, 30)];
maskPath.lineWidth = 20.f;
maskPath.lineJoinStyle = kCGLineJoinBevel;
[maskPath stroke];
// 中間第二個圖
UIBezierPath* maskFillPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(200, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(30, 30)];
maskFillPath.lineWidth = 20.f;
maskFillPath.lineJoinStyle = kCGLineJoinBevel;
[maskFillPath fill];
[maskFillPath stroke];
// 右邊第三個圖
UIBezierPath *maskLinePath = [UIBezierPath bezierPath];
maskLinePath.lineWidth = 20.f;
maskLinePath.lineCapStyle = kCGLineCapRound;
[maskLinePath moveToPoint:CGPointMake(250.0, 50)];
[maskLinePath addLineToPoint:CGPointMake(300.0, 100.0)];
[maskLinePath stroke];
}
上圖中:
1)圖一和圖二 唯一的不同是[maskFillPath fill]方法,fill方法要在封閉的曲線調用。
2)圖一和圖二 爲設定特定的角爲圓角的矩形,corners爲UIRectCornerTopLeft左上角,cornerRadii圓角大小爲30,綠色的箭頭 表示的設定的這個角。
corners 爲下面五種類型
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
UIRectCornerTopLeft = 1 << 0, // 左上角
UIRectCornerTopRight = 1 << 1, // 右上角
UIRectCornerBottomLeft = 1 << 2, // 左下角
UIRectCornerBottomRight = 1 << 3, // 右下角
UIRectCornerAllCorners = ~0UL // 全部
};
3)圖一和圖二 黃色的箭頭 設置的屬性 拐角樣式:lineJoinStyle kCGLineJoinBevel(缺角)
lineJoinStyle 爲下面三種類型
typedef CF_ENUM(int32_t, CGLineJoin) {
kCGLineJoinMiter, // 尖角
kCGLineJoinRound, // 圓角
kCGLineJoinBevel // 缺角
};
4)圖三 白色的箭頭 設置的屬性 path端點樣式:lineCapStyle kCGLineCapRound(圓形端點)
lineCapStyle 爲下面三種類型
typedef CF_ENUM(int32_t, CGLineCap) {
kCGLineCapButt, // 無端點
kCGLineCapRound, // 圓形端點
kCGLineCapSquare // 方形端點
};
有興趣的 可以試試別的方法屬性~
2.3 波浪曲線實現
如何實現 N階 波浪式曲線?如何找到 N-1 個對應的控制點?
有兩個方法,下圖爲同數據,方案一 和 方案二 分別所得曲線圖。
方案一 爲左邊(三階貝塞爾)圖 其中 第二條的紅點 爲數據的位置
方案二 爲右邊(CatmullRom)圖 其中 第二條的紅點 爲數據的位置
方案一:根據 創建三次貝塞爾曲線 方法 實現波浪曲線
控制點的選取方案不唯一,以下爲我選擇控制點的方案:
控制點P1:CGPointMake((PrePonit.x+NowPoint.x)/2, PrePonit.y)
控制點P2:CGPointMake((PrePonit.x+NowPoint.x)/2, NowPoint.y)
可以根據前一個點PrePonit 和 現在的點NowPoint 進行計算 控制點。
主要代碼如下:
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:[self pointAtIndex:0]];
NSInteger count = self.points.count;
CGPoint PrePonit;
for (NSInteger i = 0; i < count; i++) {
CGPoint NowPoint = [self pointAtIndex:i];
if(i == 0) {
PrePonit = NowPoint;
} else {
// 利用三次曲線 形成波浪曲線
[path addCurveToPoint:point controlPoint1:CGPointMake((PrePonit.x+NowPoint.x)/2, PrePonit.y) controlPoint2:CGPointMake((PrePonit.x+NowPoint.x)/2, NowPoint.y)];
PrePonit = NowPoint;
}
}
return path;
方案二:使用 CatmullRom 插值樣條。(有興趣的可以百度一下~這裏只簡單介紹)
要點分析:
1)給定一組控制點而得到一條曲線,曲線經過給定所有數據點。
2)Catmull-Rom 公式:P(t) = 0.5 * (2*p1 + (p2 - p0) *t + (2*p0 - 5*p1 + 4*p2 - p3)* t * t + (3*p1 - p0 - 3*p2 + p3) * t * t * t);
注:t爲分割的最小粒尺寸,根據 P0 P1 P2 P3 順序的四個點 求得 P1P2 曲線公式。
主要代碼如下:
void getPointsFromBezier(void *info, const CGPathElement *element) {
NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;
CGPathElementType type = element->type;
CGPoint *points = element->points;
if (type != kCGPathElementCloseSubpath) {
[bezierPoints addObject:VALUE(0)];
if ((type != kCGPathElementAddLineToPoint) &&
(type != kCGPathElementMoveToPoint))
[bezierPoints addObject:VALUE(1)];
}
if (type == kCGPathElementAddCurveToPoint)
[bezierPoints addObject:VALUE(2)];
}
NSArray *pointsFromBezierPath(UIBezierPath *bpath) {
NSMutableArray *points = [NSMutableArray array];
// 獲取貝塞爾曲線上所有的點
CGPathApply(bpath.CGPath, (__bridge void *)points, getPointsFromBezier);
return points;
}
- (UIBezierPath*)smoothedPathWithGranularity:(NSInteger)granularity path:(UIBezierPath *)path {
NSMutableArray *points = [pointsFromBezierPath(path) mutableCopy];
if (points.count < 4) return [path copy];
[points insertObject:[points objectAtIndex:0] atIndex:0];
[points addObject:[points lastObject]];
UIBezierPath *smoothedPath = [path copy];
[smoothedPath removeAllPoints];
[smoothedPath moveToPoint:POINT(0)];
for (NSUInteger index = 1; index < points.count - 2; index++) {
CGPoint p0 = POINT(index - 1);
CGPoint p1 = POINT(index);
CGPoint p2 = POINT(index + 1);
CGPoint p3 = POINT(index + 2);
for (int i = 1; i < granularity; i++) {
// granularity 這裏按照 20 粒度劃分的
float t = (float) i * (1.0f / (float) granularity);
float tt = t * t;
float ttt = tt * t;
CGPoint pi;
// 根據 CatmullRom 公式 根據 P0 P1 P2 P3 獲取點的座標
pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
if (pi.x <= self.width) {
[smoothedPath addLineToPoint:pi];
}
}
if (p2.x <= self.width) {
[smoothedPath addLineToPoint:p2];
}
}
return smoothedPath;
}
對比總結:
方案一 控制點的選取比較難,曲線的彎度 也取決於控制點,操作簡單,易理解。
方案二 曲線更順滑,但實現更復雜,不易理解。
這裏推薦兩個好用的網站:
a 這個網站提供了豐富的曲線類型可供選擇,非常直觀。
b 這個網站提供了可視化的修改兩個控制點,來生成一條三階貝塞爾曲線,並切右邊還可以看到這條曲線產生的動畫會做怎樣的速度改變。
http://www.roblaplaca.com/examples/bezierBuilder/#
3 顯示
當layer與貝塞爾曲線相遇,會發生什麼樣的神奇反應?
3.1 CALayer
蘋果官網註釋:“An object that manages image-based content and allows you to perform animations on that content.” 管理基於圖像的內容並允許您對該內容執行動畫的對象。
CALayer 主要就兩方面作用:
1) 管理展示內容
2)內容可執行動畫
CALayer 自身有很多情況下自帶隱式動畫,但是UIView的根Layer是沒有隱式動畫的。
3.1.1 常用屬性
// 圖層大小 支持隱式動畫
@property CGRect bounds;
// 圖層位置 支持隱式動畫
@property CGPoint position;
// 在z軸上的位置 支持隱式動畫
@property CGFloat zPosition;
// 沿z軸位置的錨點 支持隱式動畫
@property CGFloat anchorPointZ;
// 錨點 默認在layer的中心點 取值範圍(0~1) 支持隱式動畫
@property CGPoint anchorPoint;
// 圖層變換 支持隱式動畫
@property CATransform3D transform;
// 圖層大小和位置 不支持隱式動畫
@property CGRect frame;
// 是否隱藏 支持隱式動畫
@property(getter=isHidden) BOOL hidden;
// 圖層背景是否顯示 支持隱式動畫
@property(getter=isDoubleSided) BOOL doubleSided;
// 父圖層 支持隱式動畫
@property(nullable, readonly) CALayer *superlayer;
// 子圖層 支持隱式動畫
@property(nullable, copy) NSArray<__kindof CALayer *> *sublayers;
// 子圖層變換 支持隱式動畫
@property CATransform3D sublayerTransform;
// 圖層蒙版 支持隱式動畫
@property(nullable, strong) __kindof CALayer *mask;
// 子圖層是否裁切超出父圖層的部分,默認爲NO
@property BOOL masksToBounds;
// 圖層顯示內容 設置layer的contents可以爲layer添加顯示內容 支持隱式動畫
@property(nullable, strong) id contents;
// 圖層顯示內容的大小和位置 支持隱式動畫
@property CGRect contentsRect;
// 用於指定層的內容如何在其範圍內定位或縮放
@property(copy) CALayerContentsGravity contentsGravity;
// 是否包含完全不透明內容的布爾值
@property(getter=isOpaque) BOOL opaque;
// 背景色 支持隱式動畫
@property(nullable) CGColorRef backgroundColor;
// 圓角半徑 支持隱式動畫
@property CGFloat cornerRadius;
// 邊框寬度 支持隱式動畫
@property CGFloat borderWidth;
// 邊框顏色 支持隱式動畫
@property(nullable) CGColorRef borderColor;
// 透明度 支持隱式動畫
@property float opacity;
// 陰影顏色 支持隱式動畫
@property(nullable) CGColorRef shadowColor;
// 陰影透明度 默認爲0 需要顯示陰影 必須設置值 支持隱式動畫
@property float shadowOpacity;
// 陰影偏移量 支持隱式動畫
@property CGSize shadowOffset;
// 陰影半徑 支持隱式動畫
@property CGFloat shadowRadius;
// 陰影形狀 支持隱式動畫
@property(nullable) CGPathRef shadowPath;
3.1.2 子類
CALayer的子類有很多,下面說幾個比較常用的。
3.2 CAShapeLayer
蘋果官網註釋:“A layer that draws a cubic Bezier spline in its coordinate space.” 專門用於繪製貝塞爾曲線的layer。
3.2.1 看一下它獨特的屬性:
// path屬性是曲線的路徑,也是它和貝塞爾曲線緊密連接一個入口,決定了圖層上畫的是什麼形狀。
@property(nullable) CGPathRef path;
// 填充顏色
@property(nullable) CGColorRef fillColor;
// 曲線 指定哪塊區域爲內部,內部會被填充顏色
@property(copy) CAShapeLayerFillRule fillRule;
// 線的顏色
@property(nullable) CGColorRef strokeColor;
// strokeStart 和 strokeEnd 兩者的取值都是0~1,決定貝塞爾曲線的劃線百分比
@property CGFloat strokeStart;
@property CGFloat strokeEnd;
// 虛線開始的位置
@property CGFloat lineDashPhase;
// 虛線設置,數組中奇數位實線長度,偶數位帶遍空白長度
@property(nullable, copy) NSArray<NSNumber *> *lineDashPattern;
// 線的寬度
@property CGFloat lineWidth;
// 最大斜接長度 只有lineJoin屬性爲kCALineJoinMiter時miterLimit纔有效
@property CGFloat miterLimit;
// 線端點樣式(樣式與 貝塞爾曲線的CGLineCap 屬性一致)
@property(copy) CAShapeLayerLineCap lineCap;
// 拐角樣式(樣式與 貝塞爾曲線的CGLineJoin 屬性一致)
@property(copy) CAShapeLayerLineJoin lineJoin;
3.2.2 舉個栗子🌰
使用上面的一些屬性,再結合貝塞爾曲線,我們實現瞭如下一些效果:
其中圖五的效果,代碼實現如下:
UIBezierPath *maskPath = [UIBezierPath bezierPath];
for (NSInteger i = 1; i < 9; i++) {
UIBezierPath *tempPath = [UIBezierPath bezierPathWithRect:CGRectMake(190 - 20 * i, 550 - 10 * i, 40 * i, 20 * i)];
[maskPath appendPath:tempPath];
}
[maskPath stroke];
// CAShapeLayer
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
maskLayer.lineWidth = 5;
maskLayer.strokeColor = [UIColor purpleColor].CGColor;
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = [UIColor cyanColor].CGColor;
maskLayer.strokeStart = 0.2;
maskLayer.strokeEnd = 0.5;
maskLayer.lineDashPattern = @[@(10), @(10), @(30), @(30)];
maskLayer.lineDashPhase = 0;
[self.view.layer addSublayer:maskLayer];
3.3 CAGradientLayer
蘋果官網註釋:“A layer that draws a color gradient over its background color, filling the shape of the layer (including rounded corners)” 專門用於在背景色上繪製顏色漸變的圖層,填充圖層的形狀。
3.3.1 看一下它獨特的屬性:
// colors屬性是CAGradientLayer的特殊屬性,完美實現幾種顏色的過渡。
@property(nullable, copy) NSArray *colors;
// 定義每個梯度停止的位置。取值範圍爲0~1遞增
@property(nullable, copy) NSArray<NSNumber *> *locations;
// 決定了變色範圍的起始點
@property CGPoint startPoint;
// 決定了變色範圍的結束點
@property CGPoint endPoint;
// startPoint 和 endPoint兩者的連線決定變色的趨勢
3.3.2 舉個栗子🌰
使用上面的一些屬性我們實現瞭如下一些效果:
其中圖五的效果,代碼實現如下:
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(20, 450, 150, 150);
gradientLayer.locations = @[@(0.2), @(0.5), @(0.6), @(0.8)];
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
gradientLayer.colors = @[(id)[UIColor purpleColor].CGColor, (id)[UIColor greenColor].CGColor, (id)[UIColor orangeColor].CGColor, (id)[UIColor blackColor].CGColor];
[self.view.layer addSublayer:gradientLayer];
3.4 再舉個栗子🌰🌰
當CAGradientLayer + CAShapeLayer + 貝塞爾曲線 會有什麼效果?上代碼~
- (void)setupUI {
// 貝塞爾曲線
UIBezierPath *maskPath = [UIBezierPath bezierPath];
[maskPath moveToPoint:CGPointMake(100, 220)];
[maskPath addLineToPoint:CGPointMake(200, 150)];
[maskPath addLineToPoint:CGPointMake(300, 220)];
[maskPath stroke];
UIBezierPath *maskBottomPath = [UIBezierPath bezierPath];
[maskBottomPath moveToPoint:CGPointMake(280, 250)];
[maskBottomPath addCurveToPoint:CGPointMake(120, 250) controlPoint1:CGPointMake(250, 320) controlPoint2:CGPointMake(150, 320)];
[maskBottomPath stroke];
[maskPath appendPath:maskBottomPath];
// CAShapeLayer
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
maskLayer.lineWidth = 20;
maskLayer.strokeColor = UIColorFromRGB(0xF0F5FF).CGColor;
maskLayer.lineCap = kCALineCapRound;
maskLayer.lineJoin = kCALineJoinRound;
maskLayer.fillColor = [UIColor clearColor].CGColor;
maskLayer.strokeStart = 0;
maskLayer.strokeEnd = 0;
[self.view.layer addSublayer:maskLayer];
// CAGradientLayer
NSMutableArray *colorArray = [NSMutableArray new];
for (NSInteger i = 0; i < 6; i++) {
[colorArray addObject:[self arc4randomColor]];
}
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.bounds;
gradientLayer.colors = colorArray;
gradientLayer.startPoint = CGPointMake(0, 0.5);
gradientLayer.endPoint = CGPointMake(1, 0.5);
gradientLayer.mask = maskLayer;
[self.view.layer addSublayer:gradientLayer];
// 創建全局併發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 創建定時器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設置定時器,每N秒觸發
int64_t intervalInNanoseconds = (int64_t)(0.3 * NSEC_PER_SEC);
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)intervalInNanoseconds, 0);
// 設置定時器處理事件
dispatch_source_set_event_handler(timer, ^{
dispatch_async(dispatch_get_main_queue(), ^{
if (maskLayer.strokeEnd < 0.6) {
maskLayer.strokeEnd += 0.4;
} else if (maskLayer.strokeEnd < 0.8){
maskLayer.strokeEnd += 0.2;
} else if (maskLayer.strokeEnd < 1){
maskLayer.strokeEnd += 0.1;
} else {
maskLayer.strokeEnd = 1;
if (maskLayer.strokeStart < 0.6) {
maskLayer.strokeStart += 0.4;
} else if (maskLayer.strokeStart < 0.8){
maskLayer.strokeStart += 0.2;
} else if (maskLayer.strokeStart < 1){
maskLayer.strokeStart += 0.1;
} else {
[colorArray removeObjectAtIndex:0];
[colorArray addObject:[self arc4randomColor]];
gradientLayer.colors = colorArray;
maskLayer.strokeStart = 0;
maskLayer.strokeEnd = 0;
}
}
});
});
_timer = timer;
// 開啓定時器
dispatch_resume(_timer);
}
- (id)arc4randomColor {
return (id)[UIColor colorWithRed:arc4random()%255/255.f
green:arc4random()%255/255.f
blue:arc4random()%255/255.f
alpha:1].CGColor;
}
運行結果👇
<https://v.qq.com/x/page/l3146iykm06.html
其他layer怎麼使用?貝塞爾曲線 + Layer 還可以組合出更多神奇的反應!感興趣的可以去試試哦~本文僅爲拋磚引玉~~
本文轉載自公衆號貝殼產品技術(ID:beikeTC)。
原文鏈接: