AndroidX:新纪元的形状 (Shapes) ⭐

近期 Jetpack 全家桶推出了一个新的图形库:Graphics-Shapes  (graphics:graphics-shapes:) 。本文在这里向大家介绍它是什么、怎么使用它,希望能够帮助到大家。

图片

利用这个新的 graphics-shapes 库,我们能轻松创建和编辑复杂的圆角多边形。这个库主要有两个部分:形状 Shape 和 形变 Morph。为了照顾注意力集中时间较短的读者,我打算分两篇文章来介绍这个库,第一篇介绍 Shape 部分,它允许我们轻松创建和渲染圆角多边形;第二篇则介绍如何为这些形状制作动画效果(又称作“Morph”)。

Android 提供了非常灵活的绘制 API。在自定义 View 时,我们可以重写 onDraw(),然后用参数 canvas 来绘制任何形状,从线条到圆形、矩形,再到复杂的 Path 对象。如果想画一些简单的圆角形状,我们可以利用 RoundRect()

当然,你可以用 Path API 来实现任意复杂的圆角形状。但从开箱即用的角度来看,我们只有 Canvas.drawRoundRect() 这一个略显无力的 API,它的灵活性极小,矩形的每个圆角都是圆曲线,如果想要自定义圆角的形状或者顶点的数量,我们就得另想办法了。

终于, graphics-shapes 库诞生了,该库提供了一种简单的方式来创建各式各样的圆角形状,这也是我们创建这个库的初衷之一。

图片

当设计师给出以上形状,要求开发人员在不给切图的情况下用代码画出来,他应该拉去枪毙 🔫。但一码归一码,如果我真能用代码实现出来,我应该获奖 🏅,对吧?

言归正传,我们希望创建出来的形状,不仅适用于未来的平台(Jetpack Compose),同时也能兼容旧的 Android View 系统,这样所有的开发者都能从中受益。所以我们在内部使用 Path 对象,Path 从 Android 1.0 开始就有了,这很 “AndroidX”(强兼容性),对吧?

用于创建和绘制这些形状的 API 其实并不难,复杂的实现细节都被隐藏起来了。开发者只需要理解两个部分:1.创建多边形;2.为形状提供可选的圆角参数。别担心,我会在下面介绍这些内容。

多边形

请允许我问你一个问题:什么是“多边形”?

你不必立马回答,可以试着从多个方面思考这个问题的答案。

我想我们有必要讨论一下“多边形”的含义。在我们使用 graphics-shapes 库的时候,这个术语代表什么,还是我们理解的传统意义上的多边形吗?

维基百科上对多边形的定义是这样的:

“多边形,是平面上封闭的几何图形,或者说是由 2 条以上在同一平面的线段首尾相连组成的形状。”

额,似乎不是很有帮助,数学家果然是数学家,明明写出来的是文字,听起来却像方程式。让我们为在座的各位简化这个定义。

就其最基本的形式而言,多边形是具有边和顶点(或者“角”)的 2D 形状。人们对多边形的第一印象通常是:顶点围绕某个中心排列,所有边的长度都相等,比如下图的正五边形:

图片

然而实际上,多边形可能比这复杂得多,包括可以自相交的形状,比如下图是一个具有自我相交边界的复杂五边形:

图片

还好,我们库里面的“多边形”比较古板老套,它的顶点与某个中心等距,按顺序排列,所有的边都等长,也没有什么自相交(这个定义对处理形状之间的自动变形非常重要)。你可以将基本对象 RoundedPolygon 视为一个具有中心的形状,其所有顶点都围绕该中心,定位在距离中心给定半径处。

图片

⭐ 我们的多边形也可以再稍微复杂一点, graphics-shapes 库里面有“星形多边形”的概念,星形多边形与多边形类似,只不过它具有内半径和外半径,顶点位于其中一个半径的圆轨迹之上

图片

最后,我们的多边形具有“圆角”概念。严格来说,圆角不属于多边形概念的范畴,因为数学的多边形被定义为具有直边和尖角。因此,我们的形状命名为“圆角多边形(Rounded Polygons)”,该命名既体现了其作为多边形具有的一般概念,也体现了它带有可选圆角参数的额外差异。

圆角多边形具有和上述多边形类似的几何形状,不同之处在于每个顶点都有可选参数以描述如何将尖角圆化。例如下图是一个具有 5 条边的星形多边形,和上图类似,但外半径上的顶点所形成的角为圆角。

图片

以上就是 graphics-shapes 库可以生成的所有类型的形状:非自相交的多边形,其顶点是有序的,与中心等距,且带有可选圆角。

对库的情况有了基本了解后,现在我们来看看如何使用 API 来创建这些形状。

多边形、星形......

用于创建形状的主要类是 RoundedPolygon,你可以用这个 API 创建出许多不同的形状,但它们都归结为多边形的变体。

要创建简单的非圆角的五角星形状,只要告诉 RoundedPolygon 你想要多少个顶点,并提供可选的半径和中心点。当然任何形状都会有半径和中心,只是默认情况下,库会以 (0, 0) 为中心,1 为半径创建形状。你可以通过变换(缩放、平移、旋转)形状,使其达到所需的大小和位置:RoundedPolygon.transform(Matrix)

题外话,不知道你会不会产生疑问,为什么一个命名为 Rounded... 的 API 可以创建出来一个非圆角的东西?好吧,其实最初为了处理这种语义上的差异,我们有父类 Polygon 和子类 RoundedPolygon,但最终我们还是觉得,仅仅因为学术上的“多边形”不带圆角,就要用两个类来分割功能,这属实没必要,因此我们决定用一个类来处理所有情况。

"API 命名是困难的、不完美的,也是永远令人遗憾的根源。"

总之,API 最简单的使用方法就是传入顶点的数量,让库来完成生成形状的工作,随后可以调用 transform() 函数来调整对象的大小和位置,最后将形状写入 Path 对象中将其绘制到你的 View 中。

下面是一个简单示例,创建了一个半径为 300 像素的五边形,将生成的形状写入 Path 后,再用 Canvas 画出来。

class Pentagon(...) : View(...) {
    private val paint = /* ... */
    private val path = Path()

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        val pentagon = RoundedPolygon(
            numVertices = 5,
            radius = 300f,
            centerX = (w / 2).toFloat(),
            centerY = (h / 2).toFloat()
        )
        pentagon.toPath(path)
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawPath(path, paint)
    }
}

图片

星形多边形也很简单,但稍微特殊一点,我们要用 RoundedPolygon 类的静态方法 star() 来创建它。可以通过可选参数 innerRadius 指定内半径(默认值为 0.5)。

val pentagonalStar = RoundedPolygon.star(
 numVerticesPerRadius = 5,
    innerRadius = 180f,
    radius = 300f
)

图片

Rounding Error

我们创建了一个简单的 API 来创建和绘制正多边形和星形多边形,但即使没有 graphics-shapes 库,用现有的 API 来创建这种的直边、尖角的形状好像也不是什么难事,有趣且棘手的部分是如何将这些角圆化。

为了弄清楚这个问题,(就我而言)我重新学习了一大堆高中数学知识,包括几何学和三角学,诸如三角恒等式、余弦定理,以及像三角形的内角和为 180 度这样的几何知识都派上了用场。也许在未来我会抽时间写一下它们都被用在哪里,如果你感兴趣的话也可以翻翻代码。

对于库的使用者来说,关键在于如何使用 API 创建出让人心甘情愿花费 200 万的圆角形状。

图片

别担心,API 比实现细节简单得多,创建具有圆角的多边形或星形多边形,只不是在调用上述 API 的基础之上,再额外提供有关圆角的附加信息而已。

要完成这项任务,请使用 CornerRounding 类,它负责确定如何圆化尖角。创建该类的实例需要提供两个参数:radius 和 smoothing

半径

radius 是用于圆化顶点的圆的半径。这个参数的作用类似于 Canvas.drawRoundRect() 方法的参数 radius,只不过它和可选参数 smoothing(详见下文)共同决定圆角的效果。例如,我们可以创建这样一个圆角正三角形:

图片

其中每个角的圆角半径 r 用几何方式表示如下:

图片

半径 r 决定了圆角的尺寸。请注意,圆角半径会在顶点相交的两条直边之间的拐角处产生纯圆曲线(红色部分)。

图片

平滑拐角

和半径 radius 不同,在我们的库中 smoothing 是一个新概念。你可以将 smoothing 视为一个平滑因子,它决定了从圆角部分过渡到直边的平滑程度。

  • 平滑因子为 0(非平滑,CornerRounding 的默认值)时,会得到纯粹的圆角(当然前提是非零半径),换句话说,此时的圆角部分就只是一条纯圆曲线。

  • 非零平滑因子(最大值为 1.0)会导致圆角部分由三条独立的曲线共同组成,其中中间部分是由圆角半径生成的圆弧,也就是前面谈到的那条纯圆曲线,但这条纯圆曲线不再直接延申到多边形的直边,而是通过两条“侧翼”曲线从圆弧平滑过渡到直边,这些侧翼曲线自身是平滑(非圆形)的弧。

平滑曲线的程度决定了中间圆弧的长度(平滑度越大 ==> 圆弧越短)和侧翼曲线的长度(平滑度越大 ==> 侧翼曲线越长)。

侧翼曲线不仅影响圆角中纯圆弧部分的比例,还会影响整体圆角的长度,较大的平滑因子会将圆角与直边的的交点沿边缘推向下一个顶点。平滑因子达到 1(最大值)时,中间纯圆弧的长度为零,此时侧翼曲线的长度达到最大(最长时甚至可以延伸到下一个顶点)。

请注意,所有多边形在内部都由贝塞尔三次曲线列表表示,每条曲线由一对锚点和一对控制点定义。这些三次曲线主要用于在内部创建 Path 对象,以便稍后我们能将形状绘制到自定义 View 中。

注意:三次平面曲线的相关内容已经一定程度上脱离了本文的中心,我不想在这里过多解释它,网络上有很多关于贝塞尔曲线三次曲线路径等资料可以查阅。如果你对这些概念不熟悉,我建议你先花一点时间进行学习。这里有一个非常简单的描述,希望对你有所帮助:一条三次贝塞尔曲线可以由两个锚点(曲线的两端)和两个控制点来定义,这两个控制点能够确定锚点处曲线的斜率

译者(bqliang)曾写过一篇《Android 动画里的贝塞尔曲线》,相信对大家快速理解三次曲线会有一定的帮助。

在这里我想特别推荐一下这个贝塞尔曲线入门网站。这是一个关于贝塞尔曲线的巨大知识宝库,里面有各种证明、方程、示例代码、图表、嵌入式实时演示和详细解释。我经常打开这个网站,以进一步理解这个复杂而有趣的领域。

文字太枯燥了,让我们来看一些稍微直观点的图片吧。

  • 形状的角(左侧的白色填充对象)在右侧用绿色线条(形状的轮廓)和白色虚线(给定圆角半径的圆)表示。

  • 三次曲线由粉色圆圈(锚点)、黄色圆圈(曲线的控制点)、锚点与控制点之间的黄线表示。如果你有使用过像 Adobe Illustrator、Photoshop 等绘图程序,在绘制曲线时可能见过类似的控制手柄。

图片

平滑因子为 0(非平滑)时,会产生一条三次曲线,该曲线与拐角处具有指定圆角半径的纯圆弧重合。

当我们提供非零平滑因子时,圆角由 3 条三次曲线共同组成:圆形三次曲线(与上面的非平滑情况下看到的一样)加上两条在圆形曲线和直边之间过渡的侧翼曲线。注意,与非平滑的情况相比较,侧翼曲线在直边上的起始点更靠后。平滑的效果可能非常微妙,但它们为设计人员提供了更大的灵活性,可以生成超越传统圆角的平滑形状。

图片

呃,也许我应该提一下,3 条独立线段连接起来的曲线,竟然比一条纯圆曲线还要平滑,听上去好像不那么真实,但事实确实如此,不信你问原研哉

图片

开玩笑开玩笑,由 3 条独立线段构成的圆角看起来如此丝滑的真正原因是,每条曲线都经过计算,以匹配与下一个连接点的斜率。例如,圆角会从纯圆弧曲线平滑过渡到非圆形的平滑侧翼曲线,然后再次过渡到直边。

呼,真是一场酣畅淋漓的... 数学之旅?无论如何,总算是把 CornerRounding 类的两个参数讲完了。

One More Thing

除了上面介绍的采用顶点数量的构造函数之外,还有一个更通用的构造函数,它采用顶点列表:

fun RoundedPolygon(
    vertices: FloatArray, // 📌
    rounding: CornerRounding = CornerRounding.Unrounded,
    perVertexRounding: List<CornerRounding>? = null,
    centerX: Float = Float.MIN_VALUE,
    centerY: Float = Float.MIN_VALUE
)

FloatArray 中必须传入至少 3 个顶点的坐标,也就是说数组长度不能小于 6。

这个构造函数让您可以创建一些形状……这些形状可能不太符合我们对圆角多边形的假设。因此,如果您传入随机复杂的顶点列表,结果可能不如其他构造函数生成的更受约束的多边形那样令人满意,也不要感到惊讶。

这个构造函数存在的意义是为了创建一些更有趣的多边形形状,这些形状的顶点与中心点的距离可能并不相等。例如,我们可以使用顶点列表构造函数来创建一个底边内凹的三角形形状:

图片

这个形状的大部分是……但不完全是正三角形。所以需要使用顶点列表版本的构造函数来创建底部边缘“凹”的形状,它的代码如下:

class CustomTriangleShape(context: Context) : View(context){
    private lateinit var PointCenter : PointF
    private val paint = /* ... */
    private val path = Path()

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        PointCenter = PointF((w/2).toFloat(), (h/2).toFloat())

        val triangleInnerRadiusRatio = 38f
        val trianglePoints = listOf(
            radialToCartesian(380f, 270f.toRadians()),
            radialToCartesian(380f, 30f.toRadians()),
            radialToCartesian(triangleInnerRadiusRatio, 90f.toRadians()),
            radialToCartesian(380f, 150f.toRadians()),
        ).flatMap { listOf(it.x, it.y) }.toFloatArray()
        val triangle = RoundedPolygon(trianglePoints, CornerRounding(66f))
        triangle.toPath(path)
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawPath(path, paint)
    }

    // 角度转弧度
    private fun Float.toRadians(): Float = this * PI.toFloat() / 180f

    //根据给定的弧度角,计算在二维平面上对应的单位方向向量(单位向量长度为1)
    private fun directionVectorPointF(angleRadians: Float): PointF =
        PointF(cos(angleRadians), sin(angleRadians))

    // 极坐标转笛卡尔坐标
    private fun radialToCartesian(
        radius: Float, // 极径
        angleRadians: Float, // 极度
        center: PointF = PointCenter // 极点
    ): PointF = directionVectorPointF(angleRadians) * radius + center
}

如果看得有点吃力,建议先学习一下极坐标和笛卡尔坐标相关知识

图片

更多

前面我一直在具体讨论单一的 CornerRounding 参数的情况,但指定多个(内半径和外半径)圆角参数也是可以的,这样我们就能在星形多边形上实现类似的效果。

图片

内部顶点可以使用与外部顶点不同的圆角参数,图中的外部顶点是非纯圆角的。

fun RoundedPolygon.Companion.star(
    numVerticesPerRadius: Int,
    radius: Float = 1f,
    innerRadius: Float = .5f,
    rounding: CornerRounding = CornerRounding.Unrounded, // 📌
    innerRounding: CornerRounding? = null, // 📌
    perVertexRounding: List<CornerRounding>? = null,
    centerX: Float = 0f,
    centerY: Float = 0f
): RoundedPolygon

只要你想,还可以为每个顶点定义不同圆角参数,从而创造更个性化的形状。

各种各样千奇百怪的圆角(或非圆角)多边形形状,你都可以借助这个库将它绘制出来。当然,你一直都可以在 Android 上实现这些功能,毕竟我们也只是使用了底层的 Path API 来处理绘图过程,只是在这个过程中有很多细节(以及大量数学计算)需要解决,我们希望这个新库能让这项工作变得更加容易。

图片

上图是可以使用此库创建的形状示例,你可以在文末描述的 Github 示例中找到实现代码。

下集预告

之所以采用三次曲线作为内部结构,不仅是为了能够创建和绘制这些形状,更是为了能自动在它们之间实现平滑的动画效果,关于如何做到这一点,请查看下一篇译文——AndroidX 中的变形金刚

其他

APIs!

截至译稿发出前,该库在 AndroidX 中最新版本为 beta 1.0.0-bata01Jetpack graphics

示例代码

你在文章一开始所看到的这张图来自于示例应用,其中演示了形状的创建、编辑和变形,代码托管在 GitHub 上:ShapesDemo

该示例同时具有 Compose 和 View 应用程序,展示了如何使用该库在两种 UI 工具包中创建和变形形状。Compose 版本有一个附加的编辑器视图,有助于可视化各种形状参数。

Android Jetpack 新成员:Graphics-Shapes

作者:bqliang
链接:https://juejin.cn/post/7393551590458834979
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值