【Compose】平滑的解决圆角矩形长度过短的显示问题

最近在用到圆角矩形时发现一个问题,如果控件宽度太小,小于圆角直径的话,Compose 会做一些特殊的处理,即按照比例缩小圆角半径,让其仍然看起来是个圆角矩形,如下图。

对于有些场景,例如上面的场景,这样做就不太合理了,我们希望圆角半径仍然保持不变,就是这样。

本篇文章内容就是关于如何实现上面的效果。

Shape

本来我是想直接通过 Canvas 来绘制,但发现这么做不够通用,Compose 中一般通过 Shape 来描述形状,而上面的情况也属于一种圆角矩形形状,那么如果我可以通过定义一个能解决上述情况的 Shape 的话就可以应用在很多地方了。

SlickRoundCornerShape

Compose 中提供了一个 Shape 接口,其中包含一个方法,我们自定义 Shape 时主要就是实现这个方法。

kotlin 复制代码
@Immutable
interface Shape {
    /**
     * Creates [Outline] of this shape for the given [size].
     *
     * @param size the size of the shape boundary.
     * @param layoutDirection the current layout direction.
     * @param density the current density of the screen.
     *
     * @return [Outline] of this shape for the given [size].
     */
    fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline
}

入参就是当前控件的信息,size 是控件的大小,返回值是个 Outline

在创建 Outline 时我们可以参考已有的 RoundedCornerShape.

kotlin 复制代码
override fun createOutline(
        size: Size,
        topStart: Float,
        topEnd: Float,
        bottomEnd: Float,
        bottomStart: Float,
        layoutDirection: LayoutDirection
    ) = if (topStart + topEnd + bottomEnd + bottomStart == 0.0f) {
        Outline.Rectangle(size.toRect())
    } else {
        Outline.Rounded(
            RoundRect(
                rect = size.toRect(),
                topLeft = CornerRadius(if (layoutDirection == Ltr) topStart else topEnd),
                topRight = CornerRadius(if (layoutDirection == Ltr) topEnd else topStart),
                bottomRight = CornerRadius(if (layoutDirection == Ltr) bottomEnd else bottomStart),
                bottomLeft = CornerRadius(if (layoutDirection == Ltr) bottomStart else bottomEnd)
            )
        )
    }

我们要处理的是 RoundedCornerShape 的特殊情况,即宽度小于圆角直径的情况,所以在这基础上我们需要添加一个判断分支来处理。

ini 复制代码
if (topStart + topEnd + bottomEnd + bottomStart == 0.0f) {
    Outline.Rectangle(size.toRect())
} else if (topStart == bottomStart && size.width < (topStart * 2F)) {
    ...
} else {
    Outline.Rounded(
        RoundRect(
            rect = size.toRect(),
            topLeft = CornerRadius(if (layoutDirection == Ltr) topStart else topEnd),
            topRight = CornerRadius(if (layoutDirection == Ltr) topEnd else topStart),
            bottomRight = CornerRadius(if (layoutDirection == Ltr) bottomEnd else bottomStart),
            bottomLeft = CornerRadius(if (layoutDirection == Ltr) bottomStart else bottomEnd)
        )
    )
}

这里为了简单起见,我们只处理 topStart == bottomStart 的情形。

现在新增的 if 分支就是我们要处理的场景了。

这里还包含了两种情况:

  • 控件高度小于等于圆角直径,此时左侧只有一个半圆。
  • 控件高度大于圆角直径,此时左侧包含上下两个圆角和一条连接直线。

针对上述两种情况,我们可以通过构建不同的 Path 来实现。

scss 复制代码
val radius = topStart
val path = Path()
if (height > radius * 2) {
    buildSlickRoundCornerPath(path, size, radius)
} else {
    buildSingleArcPath(path, size, radius)
}
Outline.Generic(path)

第一种情况是比较容易处理的。

kotlin 复制代码
private fun buildSingleArcPath(path: Path, size: Size, radius: Float) {
    path.moveTo(size.width, 0F)
    path.arcTo(
        rect = Rect(0F, 0F, radius * 2F, radius * 2F),
        startAngleDegrees = 90F,
        sweepAngleDegrees = 180F,
        forceMoveTo = true,
    )
    path.close()
}

逻辑比较简单,先移动到空间的右上角,然后绘制一个圆弧,这个圆弧的半径就是圆角半径,然后再闭合这个 Path 就行了。

我们主要来看第二种情况。

比较麻烦的是需要根据空间的宽度动态调整控件绘制区域的高度。

图中黄色线断表示控件的宽度,此时我们需要计算红线的长度,然后绘制这部分的圆弧。

还好我们有老祖宗的智慧:勾股定理

arduino 复制代码
val arcHeight = sqrt(radius * radius - (radius - width) * (radius - width))

那么圆弧到控件顶部和底部的距离就是:

ini 复制代码
val yOffset = radius - arcHeight

因此上半部分的圆弧可以这么绘制出来:

ini 复制代码
path.arcTo(
    rect = Rect(
        left = 0F,
        top = yOffset,
        right = width * 2F,
        bottom = yOffset + arcHeight * 2F,
    ),
    startAngleDegrees = 180F,
    sweepAngleDegrees = 90F,
    forceMoveTo = true,
)

然后我们把 Path 移动到下面圆弧的最底端:

ini 复制代码
val bottomArcBottom = height - yOffset
path.lineTo(x = width, y = bottomArcBottom)
path.arcTo(
    rect = Rect(
        left = 0F,
        top = bottomArcBottom - arcHeight * 2,
        right = width * 2F,
        bottom = bottomArcBottom,
    ),
    startAngleDegrees = 90F,
    sweepAngleDegrees = 90F,
    forceMoveTo = true,
)

上下两个圆弧的大小一致,参数也都差不多,就不用详细介绍了。

然后再封闭 Path 即可。

lua 复制代码
path.lineTo(0F, yOffset + arcHeight)
path.close()

这样差不多就能实现上面的效果了。

使用起来也跟普通的 Shape 一样。

scss 复制代码
modifier
    .clipToBounds()
    .background(
        color = Color.Blue.copy(alpha = 0.3F),
        shape = SlickRoundedCornerShape(radius),
    )
// or
modifier
    .clip(SlickRoundedCornerShape(radius))
    .background(color = Color.Blue.copy(alpha = 0.3F))

好了,这篇文章就这么多了,实现总体上比较简单,但有些小细节需要注意,所以直接把它分享出来,大家遇到这个问题的话可以直接拿去用。

Compose 发展比较晚,生态还不是特别完善,在应对复杂需求时可能会遇到很多诸如此类的小问题,原本用 View 可以快速实现的东西到了 Compose 这里就需要花不少时间,所以 Compose 社区生态还是要靠我们开发者逐渐完善。

点击这里查看完整代码。

相关推荐
名字还没想好☜2 分钟前
React 受控输入框光标跳到末尾:格式化输入时的 selection 丢失 bug 与修复
前端·javascript·react.js·bug·react·next.js
Access开发易登软件3 分钟前
Access 怎么做前后端分离?用 Web API 读写 SQL Server
前端·数据库·人工智能·microsoft·excel·access
10share11 分钟前
React 新一代样式隔离方案 —— 编译时、零运行时、原生写法
前端·react.js
总捣什么乱116 分钟前
MySQL MySQL是怎么保证主备一致的?
android·mysql·adb
Revolution6122 分钟前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js
我要两颗404西柚40 分钟前
Stage three:VUE工程化与实战工具
前端·javascript·vue.js
光影少年1 小时前
RN 的EventEmitter 双向通信
前端·react native·react.js
zzm6281 小时前
精读 HinDroid:基于异构信息网络的安卓恶意软件检测
android
weixin_440784111 小时前
Android基础知识汇总
android·java·android studio
布鲁飞丝1 小时前
从零实现富文本编辑器#-浏览器选区与编辑器选区模型同步
java·前端·编辑器