前言
不管在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
网友评论