TextKit实现UIBezierPath不规则视图

前言

不管在iOS还是安卓中,一般要布局一个多边形或者椭圆形状的视图,总是不是能直接容易实现。这是因为,移动端的视图坐标系都是以矩形为单位建立的,所以,要实现一个不规则图形区域,就只能绘制了。另外,如果不规则区域内如何排版文案?点击响应区域如何实现仅仅path内响应呢?

实现思路

iOS中图形的绘制主要在drawRect:rect中,既然是绘制,就必须要有绘制的path,这个其实可以Ps画板绘制思路是一样的,首先要画出一个不规则路线出来。在iOS中,我们知道UIBezierPath曲线可以实现快速绘制如三角形、梯形、椭圆等曲线path出来。
关于实现文案填充在不规则区域内
本来以为这回得用到CoreText才能实现,但是个人觉得CoreText太底层了,毕竟不是重新定义排版规则,有点杀鸡用牛刀的感觉。 于是网上查阅相关资料,发现在UI应用层和底层CoreText直接还有个TextKit框架层,像UILabel、UITextView就是通过TextKit框架实现的,于是具体阅读了一下TextKit框架,发现其有个NSTextContainer的属性里面有个exclusionPaths,表示不包括在内的贝塞尔区域。good, 要的就是它。

TextKit介绍

TextKit属于UIKit framework中,在CoreText之上。它们的层次架构图:


image.png

TextKit framework里面一共有3个重要类:

  • TextContainer
  • Layout Manager
  • Text Storage
    其中,TextContainer可以指定一组文案排除显示的区域:

textView.textContainer.exclusionPaths = [circlePath]

实现的效果大致是这样:


image.png

那么,要实现文案在圆内显示的话,只需要将圆形path的反选区域path传进去,不就可以实现了吗。OK,思路有了,开始codeding

代码实现

首先,在drawRect中,组装一个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还没有设置exclusionPaths,所以我们要传一个exclusionPath,类型是一个UIBezierPath,注意,这个是不参与排版的区域,那么我们需要将传进来的path反选,实现代码:

//对某个path取反(反选区域)
+ (UIBezierPath *)revertPath:(UIBezierPath *)path inRect:(CGRect)rect {
    UIBezierPath *mainPath = [UIBezierPath bezierPathWithRect:rect];
    mainPath.usesEvenOddFillRule = YES;
    [mainPath appendPath:path];
    return mainPath;
}

然后我们把得到的反选区域传给container, 相关逻辑代码如下:

if (self.path) {
            UIBezierPath *exclusionPath = self.path;
            if (self.exclusion == NO) {
                //取反选路径
                exclusionPath = [[self class] revertPath:self.path inRect:rect];
            }
            contatiner.exclusionPaths = @[exclusionPath];
 }

最后,我们把container用UITextView去呈现,就实现了文案填充在指定封闭path内的功能了:

//用UITextView呈现文案
        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];

我们还可以设置不规则区域内的填充颜色:

//设置不规则区域的填充色
    UIBezierPath *targetPath = self.path;
    if (self.exclusion) {
        targetPath = [[self class] revertPath:self.path inRect:rect];
    }
    UIColor *fillColor = self.fillColor ? : [UIColor clearColor];
    //设置填充颜色
    [fillColor setFill];
    //根据路径填充
    [targetPath fill];

还有一个问题,点击区域
现在点击区域还是整个rect frame矩形框,那么在exclusionPath不响应点击,很简单了,重写pointInside和hitTest方法,判断点击的point是否落在path内来区别:

- (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个属性跟一般UIControl保持一致。

这样,这个控件基本上封装完毕,最后看一下.h文件的api:

@interface KWBezierView : UIView

/*************************** 初始化方法 **********************************/
/**
 创建一个指定贝塞尔路径的视图

 @param path 贝塞尔路径
 @param exclusion 如果想要path反选的路径,将此参数设为Yes
 @return 返回KWBezierView实例
 */
+ (instancetype)bezierWithPath:(UIBezierPath *)path exclusion:(BOOL)exclusion;

/**
 创建一个指定多个点围起来的多边形视图

 @param points [CGPoint(x,y),...]
 @param exclusion 如果想要path反选的路径,将此参数设为Yes
 @return 返回KWBezierView实例
 */
+ (instancetype)bezierWithPoints:(NSArray<NSValue *> *)points exclusion:(BOOL)exclusion;


/************************ 属性 *************************************/

/**
 设置填充文案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;//行间距
/**
 设置填充的富文本(可选)
 */
@property (nonatomic, strong) NSAttributedString *attrText;

/**
 设置富文本换行模式(默认NSLineBreakByTruncatingTail)
 */
@property (nonatomic, assign) NSLineBreakMode breakMode;

/**
 设置选区填充颜色
 */
@property (nonatomic, assign) UIColor *fillColor;

/**
 不规则区域添加点击事件(可选)
 */
@property (nonatomic, copy) void (^touchEvent) (id sender);

@end

注意到,初始化方法我做了2种,关于path外部生成好传进来,考虑到使用层自己绘制多边形path提高了api使用门槛,所以又提供了用户传入一个point数组的方式,组件内部转path:

+ (UIBezierPath *)pathWithPoints:(NSArray<NSValue *> *)points {
    if (points.count < 3) {
        NSLog(@"坐标点个数不足以绘制成一个完整的贝塞尔路径!");
        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 = @"点我";
    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(@"点我响应了");
    };
}

最后,附上运行效果图:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容