TextKit實(shí)現(xiàn)UIBezierPath不規(guī)則視圖

前言

不管在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)圖:


image.png

TextKit framework里面一共有3個(gè)重要類:

  • TextContainer
  • Layout Manager
  • Text Storage
    其中,TextContainer可以指定一組文案排除顯示的區(qū)域:

textView.textContainer.exclusionPaths = [circlePath]

實(shí)現(xiàn)的效果大致是這樣:


image.png

那么,要實(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)行效果圖:


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容