【iOS】气泡样式视图

前言

在一个UITextField 或者 UITextView中长按,屏幕就会出现一个辅助编辑的工具条。这个工具条的轮廓是一个带三角形箭头的圆角矩形,看起来就像是一个“气泡”样式的提示框,这是一种非常常见的形状效果。
UIKit里的UIPopoverPresentationController可以让一个Controller以这种形式展示。但是如果要对View进行操作,似乎就没有比较直接的方法,有一种解决方法是对其layermask属性进行设置。


方法一: 用图片mask

要做出这种效果,有一种速成方法。首先做一张下面这样的png图片,然后把这张图片设置为一个CALayer对象的contents,再用这个CALayermask目标Viewlayer

代码示例:

UIImage *img = [UIImage imageNamed:@"Bubble.png"];
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = myView.bounds;
maskLayer.contents = (__bridge id _Nullable)(img.CGImage);
myView.layer.mask = maskLayer;

myView.layer.backgroundColor = [UIColor purpleColor].CGColor;

很显然,这种实现方法可定制性不高,如果需要把三角形箭头的位置居中,或者把它放在矩形框的左方,又或者需要修改一下圆角的弧度……面对这些要求,可能需要做更多的png图片。

方法二: 用“笔”画出来

仔细看一下这个形状,它无非就是几条直线加上一些圆角,事实上完全可以用“笔”把它画出来。

前期准备

在开始绘制图形之前,我们需要先定位一些关键的“点”。如下图所示,灰色虚线外框是需要mask的目标视图的大小,绿色区域是最终要绘制的图形,而红色点的坐标在绘制过程中需要使用。


假定我们按照顺时针方向绘制这个图形,并且先画“箭头”,再圆角矩形框。那么我们先把这些红点按照顺时针方向编号,并且暂时把它们放进数组里points中。

除此之外,我们还要理解void CGContextAddArcToPoint(CGContextRef c, CGFloat x1, CGFloat y1, CGFloat x2, CGFloat y2, CGFloat radius)这个函数。前四个参数用来指定两个跟绘制有关的坐标点,最后一个参数用来指定圆角的半径(如果这个参数为0,则不会有圆角效果)。除了这五个参数以外,这个函数还会受一个坐标点影响,就是绘画的那支“笔”当前所在的坐标。
举个例子,先看下面这张图:

pointtApointB指定了函数的前四个参数,radius就是最后一个参数,“画笔”当前所在的坐标点就是Current point,那么上述函数根据这些参数绘制出来的曲线就是黑色的实线。

动笔

有了上面这些准备之后,终于可以动笔了。

// 获取上下文
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
CGContextRef ctx = UIGraphicsGetCurrentContext();


// 因为第一步是要画箭头处的那个“0”的地方
// 所以把“笔”放在“0”在顺时针方向顺序的上一个点: “6”
CGPoint currentPoint = [[points objectAtIndex:6] CGPointValue];
CGContextMoveToPoint(ctx, currentPoint.x, currentPoint.y);

CGPoint pointA, pointB;
CGFloat radius;

int i = 0;
while(1) {

// 整个过程需要7次循环
if (i > 6)
break;

// 箭头处(0,1,2三个点)是三个尖角,矩形框是四个圆角
radius = i < 3 ? 0 : 10;
// radius = i < 3 ? 4 : 10; // 全画成圆角
pointA = [[points objectAtIndex:i] CGPointValue];
// 画矩形框最后一个圆角的时候,pointB就是points[0]
pointB = [[points objectAtIndex:(i+1)%7] CGPointValue];

CGContextAddArcToPoint(ctx, pointA.x, pointA.y, pointB.x, pointB.y, radius);
i = i + 1;
}

// 获取path
CGContextClosePath(ctx);
CGPathRef path = CGContextCopyPath(ctx);
CGContextRelease(ctx);

// 生成layer
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = path;

// 设置目标view的layer的mask属性
myView.layer.mask = maskLayer;
代码封装

由于我觉得平时可能经常会用到这种形状效果,所以我把它封装起来了。这样每次需要做这种效果的时候,只需根据Viewsize去调用接口就可以了。

示例:

BubbleLayer *bbLayer = [[BubbleLayer alloc]initWithSize:myView.bounds.size];
// 提供的一些自定义设置
// bubbleLayer.arrowDirection = ArrowDirectionTop;
// bubbleLayer.arrowHeight = 12;
// bubbleLayer.arrowWidth = 18;
// bubbleLayer.arrowPosition = 0.3;
[myView.layer setMask:[bbLayer layer]];

封装的工作就是提供一些个性化的参数设置,比如“箭头”的高度、宽度、方向以及相对位置,还有矩形框的圆角半径这些。进一步的,还有根据这些个性化的参数和Viewsize,去计算上面说到的7个关键点。这些工作都比较简单,所以我就不再赘述了,有兴趣可以去我的仓库了解一下。

仓库链接: https://github.com/iHandle/BubbleLayer