美文网首页
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