这是一个产品由来以久的问题,结果伴随着新版本的测试上线,设计表示实在忍无可忍(明显欺负我新来的嘛),只好花时间去看看这个问题怎么回事。

圆角描边问题

首先说明设计的规范:

1.圆角长宽都为矩形高度一半

2.圆角描边宽度borderWidth为1单位

3.被点击时表框也需要变化

由于第三点,因此不能使用系统默认样式,这是业务前提

我们可以先看看系统默认样式下的表现,描边宽度为1单位(即button.layer.borderWidth = 1)

【iOS】自己画描边圆角,自己出坑爹问题

可以看到定义.layer.borderWidth为1单位的时候,宽度其实是2px

然后看一下我们自己写的按钮,同样定义描边宽度为1单位

【iOS】自己画描边圆角,自己出坑爹问题

可以看出问题在于圆角变成了只有一个像素,也就是0.5单位。

这个问题其实在版本中一直存在,只是因为描边按钮的曝光机会不大因此一直没有修改(设计其实已经多次提出)

这次在一个主要页面会有两次曝光,因此一定要解决这个问题。


原版本实现分析

首先讲一下这个有问题的圆角是怎么实现的。

为了实现点击时描线框可以改变颜色,并且考虑到效率问题,这整个描线框其实是一个继承UIImageView的view

也就是说这个描线框是按钮(继承UIButton)的backgroudView而非layer一部分,在此基础上所有对按钮的描线框、背景的设置其实都是由一个view来响应。

@interface RoundedRectangle : UIImageView
@property(nonatomic,assign) CGFloat  cornerRadius;
@property(nonatomic,assign) CGFloat  borderWidth;
@property(nonatomic,strong) UIColor *borderColor;
@end

这个定义很好理解,当上面参数变化的时候都会调用刷新背景的方法:- (void)_updateDisplay

因此我们下面聚焦这个方法

- (void)_updateDisplay
{
    if (self._backgroundColor != self._currentBackgroundColor ||
        self._borderColor     != self._currentBorderColor     ||
        self.borderWidth      != self._currentBorderWidth     ||
        self.cornerRadius     != self._currentCornerRadius) {
        // 缓存状态
        self._currentBackgroundColor = self._backgroundColor;
        self._currentBorderColor     = self._borderColor;
        self._currentBorderWidth     = self.borderWidth;
        self._currentCornerRadius    = self.cornerRadius;
        
        /**
         * 绘制背景图
         */
        CGFloat size = (self._currentCornerRadius > 0 ? self._currentCornerRadius * 2 + 1 : 3) + self._currentBorderWidth * 2;
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(size, size), NO, 0);
        CGContextRef contextRef = UIGraphicsGetCurrentContext();
        CGContextSetBlendMode(contextRef, kCGBlendModeCopy);
        // 设置透明背景色
        CGContextSetFillColorWithColor(contextRef, [UIColor clearColor].CGColor);
        CGContextFillRect(contextRef, CGRectMake(0, 0, size, size));
        // 有边框,先画边框,可以避免圆角的锯齿过于明显
        // 边框
        if (self._currentBorderWidth > 0 && nil != self._currentBorderColor) {
            // 画圆角矩形
            CGPathRef pathRef = CGPathCreateWithRoundedRect(CGRectMake(0, 0, size, size), self._currentCornerRadius + self._currentBorderWidth, self._currentCornerRadius + self._currentBorderWidth, NULL);
            CGContextAddPath(contextRef, pathRef);
            CGPathRelease(pathRef);
            CGContextSetFillColorWithColor(contextRef, self._currentBorderColor.CGColor);
            CGContextFillPath(contextRef);
        }
        // 画圆角矩形
        // 有边框,画内矩形
        CGPathRef pathRef = CGPathCreateWithRoundedRect(CGRectMake(self._currentBorderWidth, self._currentBorderWidth, size - self._currentBorderWidth * 2, size - self._currentBorderWidth * 2),
                                                        MAX(self._currentCornerRadius - self._currentBorderWidth, 0),
                                                        MAX(self._currentCornerRadius - self._currentBorderWidth, 0),
                                                        NULL);
        CGContextAddPath(contextRef, pathRef);
        CGPathRelease(pathRef);
        CGContextSetFillColorWithColor(contextRef, (nil != self._currentBackgroundColor ? self._currentBackgroundColor.CGColor : [UIColor clearColor].CGColor));
        CGContextFillPath(contextRef);
        
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        CGFloat inset = (self._currentCornerRadius > 0 ? self._currentCornerRadius : 1) + self._currentBorderWidth;
        self.image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(inset, inset, inset, inset) resizingMode:UIImageResizingModeTile];
    }
}

这里采用的是CGContext相关的绘制方法,具体用法的可以百度。

需要解释的几点:

1.这里圆角绘制的过程,是先画了一个小的圆角矩形

CGPathCreateWithRoundedRect(CGRectMake(0, 0, size, size), self._currentCornerRadius + self._currentBorderWidth, self._currentCornerRadius + self._currentBorderWidth, NULL);

这个size和背景的大小毫无关系,只取决于圆角的大小

然后通过resizableImageWithCapInsets方法放大这个圆角矩形

2.描边的绘制其实是两个圆角矩形重叠,希望里面的盖住外面的从而实现圆角描边。

两个CGPathCreateWithRoundedRect参数的变化应该很好理解,预期的效果应该是这样的

【iOS】自己画描边圆角,自己出坑爹问题

可以看到描边宽度实在没道理缩水。

但是,为什么没能达到预期呢???

把里面的圆角矩形变黑看看

【iOS】自己画描边圆角,自己出坑爹问题

可以看到这里面圆角矩形的圆角好像画的不对头啊!

核对一下代码,有点奇怪不是吗?


解决方法

先解决一下问题吧。

尝试一,起初以为是resizableImageWithCapInsets的拉伸导致了边的问题,但是对比系统默认的样式(同样圆角参数,填充颜色)发现外部的圆角矩形并没有问题,因此pass了这种想法;

尝试二,直接画外部圆角描线矩形,效果如图,更加奇怪!(其实从中应该能找到一些问题的线索)

【iOS】自己画描边圆角,自己出坑爹问题

尝试三,调整内部圆角矩形的参数。这就有一点瞎调的意思了,目的就是为了让内部圆角矩形的圆角贴合外部圆角矩形。

根据现有的图可以看到,内部的问题在于圆角半径太短,这里有着巨大的代码与表现的矛盾点,暂且认为是方法BUG。

修改内部矩形的画法

CGFloat newBorderWidth= self._currentBorderWidth*2;
        CGPathRef pathRef = CGPathCreateWithRoundedRect(CGRectMake(newBorderWidth/2, newBorderWidth/2, size - newBorderWidth, size - newBorderWidth),
                                                        MAX(self._currentCornerRadius, 0),
                                                        MAX(self._currentCornerRadius, 0),
                                                        NULL);
        CGContextAddPath(contextRef, pathRef);
        CGPathRelease(pathRef);
        CGContextSetFillColorWithColor(contextRef, (nil != self._currentBackgroundColor ? self._currentBackgroundColor.CGColor : [UIColor clearColor].CGColor));
        CGContextFillPath(contextRef);

newBorderWidth只是为了统一外部参数,不用在意

修改之后的表现,是宽度为0.5时的表现(1的懒得再截图了)

【iOS】自己画描边圆角,自己出坑爹问题

算是勉强解决问题了。


究竟为啥

其实第一个尝试应该再对比一下内部圆角矩形和系统默认样式的区别,但是即使一样可能也说明不了问题,因为系统按钮的描边可能是完全基于不同原理实现的。

我个人倾向于这是CGPathCreateWithRoundedRect的一个BUG,或者是CGContext的一个BUG,毕竟原来的参数设置在数学上来看是没有任何问题的。

其实还有继续研究的空间。

相关文章: