深挖SF和Skia源码剖析圆角独立设置setCornerRadius

Android SurfaceFlinger 圆角渲染深度剖析:从 setCornerRadius(float) 到四角独立弧度

基于 AOSP16-r4 源码分析,揭示 Transaction.setCornerRadius 在 SurfaceFlinger + Skia 渲染管线中的完整流程,以及单参数统一圆角与四角独立弧度两种 API 在源码层面的本质差异。


目录

  1. API 层:Java 侧的两个重载
  2. 数据结构层:CornerRadii 的设计
  3. Transaction 数据传递:从 Java 到 C++ 再到 Binder
  4. SF 层:LayerSnapshot 中的圆角状态处理
  5. Skia 渲染层:RRect 的生成与绘制
  6. 两种 API 的本质差异对比
  7. 总结:设计意图与最佳实践

1. API 层:Java 侧的两个重载

在 Android 框架中,Transaction.setCornerRadius 有两个公开版本:

// SurfaceControl.java - 传统单参数版本
public Transaction setCornerRadius(SurfaceControl sc, float cornerRadius) {
    checkPreconditions(sc);
    nativeSetCornerRadius(mNativeObject, sc.mNativeObject, cornerRadius);
    return this;
}

// 四参数版本 —— 独立控制每个角的弧度
public Transaction setCornerRadius(
        SurfaceControl sc, float topLeft, float topRight,
        float bottomLeft, float bottomRight) {
    checkPreconditions(sc);
    if (!com.android.graphics.surfaceflinger.flags.Flags.setClientDrawnCornerRadii()) {
        Log.w(TAG, "setCornerRadius was called but"
                   + "set_client_drawn_corner_radii flag is disabled");
    }
    nativeSetCornerRadius(mNativeObject, sc.mNativeObject,
                                topLeft, topRight, bottomLeft, bottomRight);
    return this;
}

关键观察:

  • 单参数版本是 @UnsupportedAppUsage(历史遗留),四参数版本是新增 API
  • 四参数版本受 set_client_drawn_corner_radii Feature Flag 控制
  • 两个重载最终通过 JNI 调用不同的 native 函数

2. 数据结构层:CornerRadii 的设计

无论是单参数还是四参数,在 C++ 层都收敛到同一个数据结构 gui::CornerRadii

// CornerRadii.h - 核心数据结构
class CornerRadii : public android::gui::CornerRadiiData {
public:
    // 统一圆角构造函数 —— 四个角完全相同
    explicit CornerRadii(float uniformRadius) {
        this->topLeft = toAidlVec2({uniformRadius, uniformRadius});
        this->topRight = toAidlVec2({uniformRadius, uniformRadius});
        this->bottomLeft = toAidlVec2({uniformRadius, uniformRadius});
        this->bottomRight = toAidlVec2({uniformRadius, uniformRadius});
    }

    // 四角独立构造函数 —— 每个角可以不同
    CornerRadii(float tl, float tr, float bl, float br) {
        this->topLeft = toAidlVec2({tl, tl});
        this->topRight = toAidlVec2({tr, tr});
        this->bottomLeft = toAidlVec2({bl, bl});
        this->bottomRight = toAidlVec2({br, br});
    }
    // ...
};

其底层 AIDL 定义展示了数据在进程间传递的形态:

// CornerRadiiData.aidl
parcelable CornerRadiiData {
    Vec2 topLeft;
    Vec2 topRight;
    Vec2 bottomLeft;
    Vec2 bottomRight;
}

设计要点: 每个角使用 Vec2(x, y) 而非单个 float,是为了未来支持椭圆角(elliptical corners)。当前实现中 x 和 y 始终相等(均为传入的 radius 值),但这为以后扩展留下了空间——理论上可以实现类似 CSS border-radius: 10px 20px 的效果。


3. Transaction 数据传递:从 Java 到 C++ 再到 Binder

在 C++ SurfaceComposerClient 层,两个 API 的收敛逻辑非常清晰:

// SurfaceComposerClient.cpp

// 单参数版本 —— 只是一个便捷包装
Transaction& Transaction::setCornerRadius(
        const sp<SurfaceControl>& sc, float cornerRadius) {
    return setCornerRadius(sc, gui::CornerRadii(cornerRadius));
    //                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                       直接构造统一 CornerRadii,然后委派给四角版本
}

// 四角版本 —— 真正的实现
Transaction& Transaction::setCornerRadius(
        const sp<SurfaceControl>& sc, const gui::CornerRadii& radii) {
    layer_state_t* s = getLayerState(sc);
    if (!s) {
        mStatus = BAD_INDEX;
        return *this;
    }
    s->what |= layer_state_t::eCornerRadiusChanged;
    s->cornerRadii = radii;
    return *this;
}

数据流:

  1. layer_state_t 被打包进 Transaction 的 Binder 消息
  2. SurfaceFlinger 进程接收后解析 layer_state_t
  3. 通过 eCornerRadiusChanged flag 触发 SF 侧的圆角更新逻辑

4. SF 层:LayerSnapshot 中的圆角状态处理

当 SurfaceFlinger 收到圆角变化的 Transaction 后,会在 LayerSnapshotBuilder 中进行处理:

// LayerSnapshotBuilder.cpp

// 触发条件:eCornerRadiusChanged 或 Geometry 变化
if (forceUpdate || snapshot.clientChanges & layer_state_t::eCornerRadiusChanged ||
    snapshot.changes.any(RequestedLayerState::Changes::Geometry |
                         RequestedLayerState::Changes::BufferUsageFlags) ||
    snapshot.clientChanges & layer_state_t::eClientDrawnCornerRadiusChanged) {
    updateRoundedCorner(snapshot, requested, parentSnapshot, args);
}

updateRoundedCorner 函数处理了以下复杂逻辑:

4.1 父 Layer 圆角继承

void LayerSnapshotBuilder::updateRoundedCorner(LayerSnapshot& snapshot,
                                               const RequestedLayerState& requested,
                                               const LayerSnapshot& parentSnapshot,
                                               const Args& args) {
    // 1. 将父 layer 的圆角状态变换到当前 layer 的本地坐标空间
    ui::Transform t = snapshot.localTransform.inverse();
    parentSettings.cropRect = t.transform(parentSnapshot.roundedCorner.cropRect);
    parentSettings.radii.transform(t);  // 对四个角分别做 scale 变换

    // 2. 当前 layer 自身的设置
    layerSettings.radii = requested.cornerRadii;
    layerSettings.cropRect = snapshot.croppedBufferSize;

    // 3. 优先级决策:父 crop 完全在 layer crop 内部 → 用父设置
    //    否则 → 用 layer 自身设置
    if (parentSettingsValid && layerSettingsValid) {
        if (parentCropInsideLayerCrop) {
            snapshot.roundedCorner = parentSettings;
        } else {
            snapshot.roundedCorner = layerSettings;
        }
    }
    // ...
}

4.2 子 Layer 与父圆角区域的重叠检测

childOverlapsParentCornerRegion 是一个关键优化函数,它检查子 Layer 是否与父 Layer 的圆角区域重叠:

bool LayerSnapshotBuilder::childOverlapsParentCornerRegion(
        const FloatRect& childCropRect, const FloatRect& parentCropRect,
        const gui::CornerRadii& parentRadii) {

    // 根据四个独立的 corner radius 算出四个角区域
    FloatRect parentCornerRegionTL(
        parentCropRect.left, parentCropRect.top,
        parentCropRect.left + parentRadii.topLeft.x,    // 使用 topLeft
        parentCropRect.top + parentRadii.topLeft.y);

    FloatRect parentCornerRegionTR(
        parentCropRect.right - parentRadii.topRight.x,  // 使用 topRight
        parentCropRect.top, parentCropRect.right,
        parentCropRect.top + parentRadii.topRight.y);
    // ... BL, BR 同理

    // 如果 child 与任何父角区域重叠,则需继承父的圆角设置
    return child 与任一 cornerRegion 有交集;
}

这里正是四角独立弧度的核心价值体现:如果使用单参数 API(四个角统一),所有角区域大小一致;使用四参数 API,每个角的裁剪区域可以不同,从而让子 Layer 更精确地判断是否需要继承圆角。

4.3 Client Drawn Corner Radius 优化

如果 App 端自己绘制了圆角(client drawn),SF 检测到 client 的圆角参数与 SF 计算的完全一致时,会跳过 SF 侧的圆角绘制:

if (!requested.clientDrawnCornerRadii.isEmpty() &&
    requested.clientDrawnCornerRadii == snapshot.roundedCorner.croppedRequestedRadii &&
    snapshot.geomLayerBounds == requested.clientDrawnCornerRadiusCrop) {
    // 客户端已经画了圆角,SF 不用重复画
    snapshot.roundedCorner.radii = gui::CornerRadii(0.f);
}

5. Skia 渲染层:RRect 的生成与绘制

到了最核心的部分——Skia 如何将 CornerRadii 转换为实际的圆角渲染。

先说结论:Skia 内部用 SkVector fRadii[4] 原生存储四个独立半径,从头到尾从未合并。单参数和四参数的区别仅在于这 4 个值是否相等。

5.1 SkVector 数组的生成

SF 的 RenderEngine 将 CornerRadii 映射为 Skia 的 SkVector[4],顺序为 TL → TR → BR → BL:

// SkiaRenderEngine.cpp
static inline std::array<SkVector, 4> getCornerRadiiAsSkVector(
        const android::gui::CornerRadii& radii) {
    std::array<SkVector, 4> skVector;
    skVector[0].set(radii.topLeft.x,     radii.topLeft.y);
    skVector[1].set(radii.topRight.x,    radii.topRight.y);
    skVector[2].set(radii.bottomRight.x, radii.bottomRight.y);
    skVector[3].set(radii.bottomLeft.x,  radii.bottomLeft.y);
    return skVector;
}

5.2 getBoundsAndClip:两种渲染策略

SF 侧通过 getBoundsAndClip 返回一对 (bounds, clip) RRect,在 “直接画圆角矩形” 和 “clip 裁剪” 两种策略之间选择:

// SkiaRenderEngine.cpp
static inline std::pair<SkRRect, SkRRect> getBoundsAndClip(
        const FloatRect& boundsRect, const FloatRect& cropRect,
        const gui::CornerRadii& cornerRadii) {
    // ...
    if (!cornerRadii.isEmpty()) {
        // 策略 A: bounds==crop → 直接画带四角独立半径的 RRect,无需 clip
        if (bounds == crop || crop.isEmpty()) {
            SkRRect rrect;
            rrect.setRectRadii(bounds, radii.data());
            return {rrect, {}};  // ← clip 为空
        }

        // 策略 B: crop 内含 bounds,且内缩后仍包含 → 说明 crop 圆角超出内容区,直接用矩形
        if (crop.contains(bounds)) {
            float dx = std::min({cornerRadii.topLeft.x, cornerRadii.topRight.x,
                                 cornerRadii.bottomLeft.x, cornerRadii.bottomRight.x});
            float dy = std::min({...});
            if (crop.makeInset(dx, dy).contains(bounds))
                return {SkRRect::MakeRect(bounds), {}};  // 无需圆角
            // 尝试将 bounds∩crop 合并为单个 RRect
            if (intersectionIsRoundRect(...))
                return {intersectionBounds, {}};
        }

        // 策略 C (兜底): 用 crop 构造带圆角的 clip RRect
        clip.setRectRadii(crop, radii.data());
    }
    return {SkRRect::MakeRect(bounds), clip};
}

5.3 Skia 内部的 setRectRadii:存数据 + 重叠修正 + 类型分类

setRectRadii 是 Skia SkRRect 的核心入口,三步完成:

// external/skia/src/core/SkRRect.cpp
void SkRRect::setRectRadii(const SkRect& rect, const SkVector radii[4]) {
    if (!this->initializeRect(rect)) return;       // ① 存矩形
    memcpy(fRadii, radii, sizeof(fRadii));          // ② 存四个独立 SkVector
    if (clamp_to_zero(fRadii)) { setRect(rect); return; }
    this->scaleRadii();                             // ③ W3C 圆角重叠修正
    this->computeType();                            // ④ 形状分类
}

核心数据结构就是一个数组:

// external/skia/include/core/SkRRect.h
SkRect  fRect = SkRect::MakeEmpty();
SkVector fRadii[4] = {{0,0}, {0,0}, {0,0}, {0,0}};  // UL, UR, LR, LL
int32_t fType = kEmpty_Type;
5.3.1 scaleRadii:W3C CSS 圆角重叠算法

如果两个相邻角的半径之和超过了边长,按 CSS Backgrounds Level 3 规范等比例缩小所有半径:

设置 rect=[0,0,200,100], radii=[(80,80),(80,80),(80,80),(80,80)]

检查顶边: fRadii[0].fX + fRadii[1].fX = 80+80 = 160 ≤ 200 ✓ 不缩放
检查右边: fRadii[1].fY + fRadii[2].fY = 80+80 = 160 > 100 ✗ f = 100/160 = 0.625

所有半径 × 0.625 → 最终每个角 = (50, 50)

源码实现:

// SkRRect.cpp
bool SkRRect::scaleRadii() {
    double scale = 1.0;
    double width = fRect.fRight - fRect.fLeft;
    double height = fRect.fBottom - fRect.fTop;

    scale = compute_min_scale(fRadii[0].fX, fRadii[1].fX, width,  scale);  // 顶边
    scale = compute_min_scale(fRadii[1].fY, fRadii[2].fY, height, scale);  // 右边
    scale = compute_min_scale(fRadii[2].fX, fRadii[3].fX, width,  scale);  // 底边
    scale = compute_min_scale(fRadii[3].fY, fRadii[0].fY, height, scale);  // 左边

    if (scale < 1.0) {
        fRadii[0].fX *= scale; fRadii[1].fX *= scale;  // 顶边两个角的X分量
        fRadii[1].fY *= scale; fRadii[2].fY *= scale;  // 右边两个角的Y分量
        fRadii[2].fX *= scale; fRadii[3].fX *= scale;  // 底边两个角的X分量
        fRadii[3].fY *= scale; fRadii[0].fY *= scale;  // 左边两个角的Y分量
    }
    this->computeType();
    return scale < 1.0;
}
5.3.2 computeType:形状分类
// SkRRect.cpp
void SkRRect::computeType() {
    bool allRadiiEqual = true;
    bool allCornersSquare = (fRadii[0].fX == 0 || fRadii[0].fY == 0);

    for (int i = 1; i < 4; ++i) {
        if (fRadii[i].fX != 0 && fRadii[i].fY != 0) allCornersSquare = false;
        if (fRadii[i] != fRadii[i-1]) allRadiiEqual = false;
    }

    if (allCornersSquare)         fType = kRect_Type;     // 全是直角
    else if (allRadiiEqual)       fType = kSimple_Type;   // 四个角相同
    else                          fType = kComplex_Type;  // ★ 四个角不同
}

四参数 API → 四角值不同 → kComplex_Type。这个 type 决定了 GPU 走优化的 simple shader 还是通用 complex shader,但对渲染结果和性能无明显影响

5.4 从 RRect 到 GPU 图元:四段圆锥曲线

canvas->clipRRect()drawRRect() 真正需要光栅化时,Skia 将 RRect 展开为 path。关键代码在 SkPath_RRectPointIterator

// external/skia/src/core/SkPathMakers.h
class SkPath_RRectPointIterator {
public:
    SkPath_RRectPointIterator(const SkRRect& rrect, ...) {
        const SkScalar L = bounds.fLeft, T = bounds.fTop;
        const SkScalar R = bounds.fRight, B = bounds.fBottom;

        fPts[0] = {L + rrect.radii(kUpperLeft_Corner).fX,  T};      // 顶边左端点
        fPts[1] = {R - rrect.radii(kUpperRight_Corner).fX, T};      // 顶边右端点
        fPts[2] = {R, T + rrect.radii(kUpperRight_Corner).fY};      // 右边上端点
        fPts[3] = {R, B - rrect.radii(kLowerRight_Corner).fY};      // 右边下端点
        fPts[4] = {R - rrect.radii(kLowerRight_Corner).fX, B};      // 底边右端点
        fPts[5] = {L + rrect.radii(kLowerLeft_Corner).fX,  B};      // 底边左端点
        fPts[6] = {L, B - rrect.radii(kLowerLeft_Corner).fY};       // 左边下端点
        fPts[7] = {L, T + rrect.radii(kUpperLeft_Corner).fY};       // 左边上端点
    }
};

8 个点两两一组定义 4 个角的弧区域,每个点都用自己的 rrect.radii(corner) 计算偏移。

最终生成 path:

move → line → conic(w=0.707) → line → conic(w=0.707) → ... × 4 → close
       ↑                 ↑
    直线段            圆锥曲线(有理二次贝塞尔)
                     weight = cos(45°) = √2/2,精确画 90° 圆弧

弧形几何示意:

        P1 (矩形角点)
          ·
         / \
        /   \     ← conic 曲线, weight=0.707
    P0 ····· P2
    (由 fRadii[i]   (由 fRadii[i]
    算出的弧起点)    算出的弧终点)

5.5 实际绘制

回到 SF 的 RenderEngine,最终的绘制逻辑:

// SkiaRenderEngine.cpp
const auto [bounds, roundRectClip] =
    getBoundsAndClip(layer.geometry.boundaries,
                     layer.geometry.roundedCornersCrop,
                     layer.geometry.roundedCornersRadii);

// Step 1: clip 裁剪(策略 C)
if (!roundRectClip.isEmpty()) {
    canvas->clipRRect(roundRectClip, enableAntiAlias);
}

// Step 2: 绘制内容
if (!bounds.isRect()) {
    // 策略 A:bounds 自带四角独立圆角,直接画 RRect
    paint.setAntiAlias(true);
    canvas->drawRRect(bounds, paint);
} else {
    // 策略 B/C:bounds 是纯矩形(clip 处理了圆角)
    canvas->drawRect(bounds.rect(), paint);
}

一句话总结 Skia 层的行为:四个角从 fRadii[0..3] 到 8 个弧端点再到 4 段 conic 曲线,每个角的半径始终独立存储、独立读取、独立生成几何。单参数和四参数的代码路径完全一致。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里马学框架

帮助你了,就请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值