美文网首页
TextKit实现UIBezierPath不规则视图

TextKit实现UIBezierPath不规则视图

作者: 许久以前 | 来源:发表于2019-03-01 17:17 被阅读52次

    前言

    不管在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

    相关文章

      网友评论

          本文标题:TextKit实现UIBezierPath不规则视图

          本文链接:https://www.haomeiwen.com/subject/kjdduqtx.html