Android SurfaceFlinger 圆角渲染深度剖析:从 setCornerRadius(float) 到四角独立弧度
基于 AOSP16-r4 源码分析,揭示 Transaction.setCornerRadius 在 SurfaceFlinger + Skia 渲染管线中的完整流程,以及单参数统一圆角与四角独立弧度两种 API 在源码层面的本质差异。
目录
- API 层:Java 侧的两个重载
- 数据结构层:CornerRadii 的设计
- Transaction 数据传递:从 Java 到 C++ 再到 Binder
- SF 层:LayerSnapshot 中的圆角状态处理
- Skia 渲染层:RRect 的生成与绘制
- 两种 API 的本质差异对比
- 总结:设计意图与最佳实践
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_radiiFeature 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;
}
数据流:
layer_state_t被打包进 Transaction 的 Binder 消息- SurfaceFlinger 进程接收后解析
layer_state_t - 通过
eCornerRadiusChangedflag 触发 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 曲线,每个角的半径始终独立存储、独立读取、独立生成几何。单参数和四参数的代码路径完全一致。

122

被折叠的 条评论
为什么被折叠?



