平滑圆角在 iOS 7 中首次出现,并且在之后 iOS 中都使用了平滑圆角。平滑圆角的过度看起来比普通圆角更舒服,所以设计师希望我们使用平滑圆角替换原来的普通圆角,但 Android 原接口不支持平滑圆角,所以有了该文档。
平滑圆角与普通圆角
自从在 iOS 7 出现了平滑圆角之后,网络上研究平滑圆角算法的文章很多,从超椭圆到 Figma 的方圆。目前看 Figma 的方圆最接近 iOS 平滑圆角。Figma 写了一篇博客文章「不顾一切寻找方圆」介绍了 Figma 寻找平滑圆角算法的过程。文章比较难理解,文中还有不少数学方程。小米的设计师 MartinRGB 翻译了该文章,并且编写了 demo 程序,同时在 Github 开源了。最后,我们选择了 MartinRGB 开源的算法。
Figma 的算法有一个缺陷,它的圆角半径在增大时,平滑圆角会慢慢退化,直到圆角半径等于短边的一半时(暂且称为胶囊形状),会完全退化成普通圆角,但 iOS 的胶囊形状还是平滑圆角。奇怪的是,目前市面上的设计软件画出的胶囊形状都会退化成普通圆角,我们不清楚什么原因。
退化的胶囊形状
iOS 的胶囊形状
我们先从 demo 中寻找平滑圆角退化的原因。一个平滑圆角由 3 个部分组成,以下图为例,圆点 1 和圆点 2 之间是贝塞尔曲线,圆点 2 和圆点 3 之间是圆弧,圆点 3 和圆点 4 之间是贝塞尔曲线。而普通圆角中没有贝塞尔曲线,只有圆弧。
当圆角半径变大时,圆点 4 向下移动,圆点 5 向上移动,直到两个圆点相遇,之后不再移动。因为圆点 1 和圆点 4 是对称的,所以当圆点 4 和圆点 5 相遇后,圆点 1 和圆点 8 也不再移动了。这会导致了圆角中圆弧长度越来越长,而贝塞尔曲线的长度越来越小,直到贝塞尔曲线长度是 0,这时完全退化成普通圆角了,如下图所示。
我们分析影响圆点 1 位置的变量有哪些,即贝塞尔曲线的起始点。下面代码是计算中间变量的算法。
var shorter_l = Math.min(size.width, size.height);
var p, l, a, b, c, d;
var angle_alpha, angle_beta, angle_theta;
var d_div_c, h_longest;
p = Math.min(shorter_l / 2, (1 + smoothness) * radius);
if (radius > shorter_l / 4) {
var change_percentage = (radius - shorter_l / 4) / (shorter_l / 4);
angle_beta = 90 * (1 - smoothness * (1 - change_percentage));
angle_alpha = 45 * smoothness * (1 - change_percentage);
} else {
angle_beta = 90 * (1 - smoothness);
angle_alpha = 45 * smoothness;
}
angle_theta = (90 - angle_beta) / 2;
d_div_c = Math.tan(angle_alpha * ANGLE_TO_RADIANS);
h_longest = radius * Math.tan((angle_theta / 2) * ANGLE_TO_RADIANS);
l = Math.sin((angle_beta / 2) * ANGLE_TO_RADIANS) * radius * Math.pow(2, 1 / 2);
c = h_longest * Math.cos(angle_alpha * ANGLE_TO_RADIANS);
d = c * d_div_c;
b = (p - l - (1 + d_div_c) * c) / 3;
a = 2 * b;
// 圆点 1 的坐标: (x + width - p - a, y)
影响圆点 1 的 X 坐标的变量有 p
和 a
,而影响 p
和 a
的只有 shorter_l
和 radius
(假设 smoothness
相同)。变量 shorter_l
是宽和高中的较短边,那我们不使用较短边,而使用当前圆点所在的边长来计算就可以解决胶囊形状退化普通圆角的问题。
上图中右上圆角和右下圆角是使用了当前边作为参数来计算的,左上圆角和左下圆角还是使用短边作为参数来计算的。可以看到圆点 1 和圆点 8 的位置向左移动了不少距离,因为它们是用宽作为参数来计算的。
但这个胶囊形状与 iOS 的胶囊形状不太一样,平滑的有点过了。因此,我们引入了一个变量来控制作为参数长边值不会太大,算法如下所示,我们对长边和短边的差值乘了一个变量 longSideAdapt
,它的范围是 0.0 - 1.0。
var shorter_l = Math.min(size.width, size.height);
var longer_l = Math.max(size.width, size.height);
longer_l = shorter_l + (longer_l - shorter_l) * longSideAdapt;