前言
不管在iOS還是安卓中,一般要布局一個(gè)多邊形或者橢圓形狀的視圖,總是不是能直接容易實(shí)現(xiàn)。這是因?yàn)?,移?dòng)端的視圖坐標(biāo)系都是以矩形為單位建立的,所以,要實(shí)現(xiàn)一個(gè)不規(guī)則圖形區(qū)域,就只能繪制了。另外,如果不規(guī)則區(qū)域內(nèi)如何排版文案?點(diǎn)擊響應(yīng)區(qū)域如何實(shí)現(xiàn)僅僅path內(nèi)響應(yīng)呢?
實(shí)現(xiàn)思路
iOS中圖形的繪制主要在drawRect:rect中,既然是繪制,就必須要有繪制的path,這個(gè)其實(shí)可以Ps畫板繪制思路是一樣的,首先要畫出一個(gè)不規(guī)則路線出來。在iOS中,我們知道UIBezierPath曲線可以實(shí)現(xiàn)快速繪制如三角形、梯形、橢圓等曲線path出來。
關(guān)于實(shí)現(xiàn)文案填充在不規(guī)則區(qū)域內(nèi)
本來以為這回得用到CoreText才能實(shí)現(xiàn),但是個(gè)人覺得CoreText太底層了,畢竟不是重新定義排版規(guī)則,有點(diǎn)殺雞用牛刀的感覺。 于是網(wǎng)上查閱相關(guān)資料,發(fā)現(xiàn)在UI應(yīng)用層和底層CoreText直接還有個(gè)TextKit框架層,像UILabel、UITextView就是通過TextKit框架實(shí)現(xiàn)的,于是具體閱讀了一下TextKit框架,發(fā)現(xiàn)其有個(gè)NSTextContainer的屬性里面有個(gè)exclusionPaths,表示不包括在內(nèi)的貝塞爾區(qū)域。good, 要的就是它。
TextKit介紹
TextKit屬于UIKit framework中,在CoreText之上。它們的層次架構(gòu)圖:

TextKit framework里面一共有3個(gè)重要類:
- TextContainer
- Layout Manager
- Text Storage
其中,TextContainer可以指定一組文案排除顯示的區(qū)域:
textView.textContainer.exclusionPaths = [circlePath]
實(shí)現(xiàn)的效果大致是這樣:

那么,要實(shí)現(xiàn)文案在圓內(nèi)顯示的話,只需要將圓形path的反選區(qū)域path傳進(jìn)去,不就可以實(shí)現(xiàn)了嗎。OK,思路有了,開始codeding
代碼實(shí)現(xiàn)
首先,在drawRect中,組裝一個(gè)TextStorage:
//填充文本
self.layout = [[NSLayoutManager alloc] init];
self.storage = [[NSTextStorage alloc] initWithAttributedString:attributeText];
[self.storage addLayoutManager:self.layout];
NSTextContainer *contatiner = [[NSTextContainer alloc] initWithSize:rect.size];
[self.layout addTextContainer:contatiner];
上面的contatiner還沒有設(shè)置exclusionPaths,所以我們要傳一個(gè)exclusionPath,類型是一個(gè)UIBezierPath,注意,這個(gè)是不參與排版的區(qū)域,那么我們需要將傳進(jìn)來的path反選,實(shí)現(xiàn)代碼:
//對(duì)某個(gè)path取反(反選區(qū)域)
+ (UIBezierPath *)revertPath:(UIBezierPath *)path inRect:(CGRect)rect {
UIBezierPath *mainPath = [UIBezierPath bezierPathWithRect:rect];
mainPath.usesEvenOddFillRule = YES;
[mainPath appendPath:path];
return mainPath;
}
然后我們把得到的反選區(qū)域傳給container, 相關(guān)邏輯代碼如下:
if (self.path) {
UIBezierPath *exclusionPath = self.path;
if (self.exclusion == NO) {
//取反選路徑
exclusionPath = [[self class] revertPath:self.path inRect:rect];
}
contatiner.exclusionPaths = @[exclusionPath];
}
最后,我們把container用UITextView去呈現(xiàn),就實(shí)現(xiàn)了文案填充在指定封閉path內(nèi)的功能了:
//用UITextView呈現(xiàn)文案
if (self.textView) {
[self.textView removeFromSuperview];
self.textView = nil;
}
self.textView = [[UITextView alloc] initWithFrame:rect textContainer:contatiner];
self.textView.scrollEnabled = NO;
self.textView.editable = NO;
self.textView.backgroundColor = [UIColor clearColor];
[self addSubview:self.textView];
我們還可以設(shè)置不規(guī)則區(qū)域內(nèi)的填充顏色:
//設(shè)置不規(guī)則區(qū)域的填充色
UIBezierPath *targetPath = self.path;
if (self.exclusion) {
targetPath = [[self class] revertPath:self.path inRect:rect];
}
UIColor *fillColor = self.fillColor ? : [UIColor clearColor];
//設(shè)置填充顏色
[fillColor setFill];
//根據(jù)路徑填充
[targetPath fill];
還有一個(gè)問題,點(diǎn)擊區(qū)域
現(xiàn)在點(diǎn)擊區(qū)域還是整個(gè)rect frame矩形框,那么在exclusionPath不響應(yīng)點(diǎn)擊,很簡單了,重寫pointInside和hitTest方法,判斷點(diǎn)擊的point是否落在path內(nèi)來區(qū)別:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if ([[self targetPath] containsPoint:point]) {
if (self.userInteractionEnabled == NO) {
return NO;
}
if (self.alpha <= 0) {
return NO;
}
if (self.hidden) {
return NO;
}
return YES;
}else {
return NO;
}
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
return self;
}else {
return [super hitTest:point withEvent:event];
}
}
上面userInteractionEnabled、hidden、alpha3個(gè)屬性跟一般UIControl保持一致。
這樣,這個(gè)控件基本上封裝完畢,最后看一下.h文件的api:
@interface KWBezierView : UIView
/*************************** 初始化方法 **********************************/
/**
創(chuàng)建一個(gè)指定貝塞爾路徑的視圖
@param path 貝塞爾路徑
@param exclusion 如果想要path反選的路徑,將此參數(shù)設(shè)為Yes
@return 返回KWBezierView實(shí)例
*/
+ (instancetype)bezierWithPath:(UIBezierPath *)path exclusion:(BOOL)exclusion;
/**
創(chuàng)建一個(gè)指定多個(gè)點(diǎn)圍起來的多邊形視圖
@param points [CGPoint(x,y),...]
@param exclusion 如果想要path反選的路徑,將此參數(shù)設(shè)為Yes
@return 返回KWBezierView實(shí)例
*/
+ (instancetype)bezierWithPoints:(NSArray<NSValue *> *)points exclusion:(BOOL)exclusion;
/************************ 屬性 *************************************/
/**
設(shè)置填充文案style(下面5種屬性和富文本屬性二選一即可)
*/
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) NSTextAlignment textAlignment;
@property (nonatomic, assign) CGFloat lineSpace;//行間距
/**
設(shè)置填充的富文本(可選)
*/
@property (nonatomic, strong) NSAttributedString *attrText;
/**
設(shè)置富文本換行模式(默認(rèn)NSLineBreakByTruncatingTail)
*/
@property (nonatomic, assign) NSLineBreakMode breakMode;
/**
設(shè)置選區(qū)填充顏色
*/
@property (nonatomic, assign) UIColor *fillColor;
/**
不規(guī)則區(qū)域添加點(diǎn)擊事件(可選)
*/
@property (nonatomic, copy) void (^touchEvent) (id sender);
@end
注意到,初始化方法我做了2種,關(guān)于path外部生成好傳進(jìn)來,考慮到使用層自己繪制多邊形path提高了api使用門檻,所以又提供了用戶傳入一個(gè)point數(shù)組的方式,組件內(nèi)部轉(zhuǎn)path:
+ (UIBezierPath *)pathWithPoints:(NSArray<NSValue *> *)points {
if (points.count < 3) {
NSLog(@"坐標(biāo)點(diǎn)個(gè)數(shù)不足以繪制成一個(gè)完整的貝塞爾路徑!");
return nil;
}
UIBezierPath *path = [UIBezierPath bezierPath];
NSValue *firstPointValue = [points firstObject];
CGPoint firstPoint = [firstPointValue CGPointValue];
[path moveToPoint:firstPoint];
for (NSInteger i=1; i<points.count; i++) {
NSValue *pointValue = points[I];
CGPoint point = [pointValue CGPointValue];
[path addLineToPoint:point];
}
[path closePath];
return path;
}
最后看外部使用demo:
- (void)demo2 {
NSValue *point1 = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
NSValue *point2 = [NSValue valueWithCGPoint:CGPointMake(40, 0)];
NSValue *point3 = [NSValue valueWithCGPoint:CGPointMake(0, 40)];
KWBezierView *bl = [KWBezierView bezierWithPoints:@[point1,point2,point3] exclusion:YES];
bl.fillColor = [UIColor redColor];
bl.text = @"點(diǎn)我";
bl.font = [UIFont systemFontOfSize:18];
bl.textColor = [UIColor whiteColor];
[self.view addSubview:bl];
[bl mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(20);
make.top.equalTo(self.view).offset(320);
make.width.mas_equalTo(100);
make.height.mas_equalTo(40);
}];
bl.touchEvent = ^(id _Nonnull sender) {
NSLog(@"點(diǎn)我響應(yīng)了");
};
}
最后,附上運(yùn)行效果圖:
