HarmonyOS 7 阴影迁移:radius=0不再代表关闭,别把textShadow一起改了
HarmonyOS 7 阴影迁移:radius=0不再代表关闭,别把textShadow一起改了
一个原本扁平的按钮,提升target版本后右下角多出一块硬边轮廓。颜色、尺寸、偏移量都没有改,截图对比却过不了。此时先搜索shadow里的radius: 0:在API26的新规则中,零代表“不模糊”,不再代表“没有阴影”。
这类适配不适合全局替换。组件shadow、设计系统中的“无阴影”令牌、刻意设计的硬阴影和Text.textShadow,需要分别判断。下面给出一个语义映射器和一份迁移清单,避免修好按钮又改坏文字。
本文依据华为API26 Beta1变更页,页面更新于2026年8月19日,2026年9月28日核对。映射代码的断言在宿主环境运行通过;ArkUI片段用于设备复现,本机没有完成API26编译和真机截图比对。配图是规则示意,不是实测截图。
三个值,现在是三种意图
此项只在应用targetSdkVersion大于等于26.0.0时生效。旧行为下,radius为0没有阴影,负数也按0处理。新行为下,radius为0保留阴影轮廓,但没有模糊;负数才表示无阴影。正数仍用于模糊阴影。
| 组件shadow的radius | 旧行为 | target达到26后的行为 |
|---|---|---|
| 小于0 | 无阴影 | 无阴影 |
| 等于0 | 无阴影 | 有阴影,无模糊 |
| 大于0 | 模糊阴影 | 模糊阴影 |
偏移量是定位问题的好帮手。如果阴影完全藏在不透明组件后方,零半径的差异可能不容易看见。将offsetX和offsetY临时设成明显的值,比只盯着radius更容易分辨“没有阴影”和“阴影没有模糊”。测试完要恢复设计值。

下面是最小复现页面,不是整个产品的推荐视觉设计:
@Entry
@Component
struct ShadowCheck {
build() {
Column({ space: 32 }) {
Text('负数:关闭').padding(20).backgroundColor(Color.White)
.shadow({ radius: -1, color: Color.Black, offsetX: 12, offsetY: 12 })
Text('零:硬边').padding(20).backgroundColor(Color.White)
.shadow({ radius: 0, color: Color.Black, offsetX: 12, offsetY: 12 })
Text('正数:模糊').padding(20).backgroundColor(Color.White)
.shadow({ radius: 12, color: Color.Black, offsetX: 12, offsetY: 12 })
}.width('100%').height('100%').padding(40).backgroundColor('#EEEEEE')
}
}
案例一:设计系统把none存成0
组件库常把none、小、中、大几个等级映射成数字。问题不在某一个按钮,而在none对应了旧平台的实现细节。继续让业务页面自行传0,下次规格变化仍要全项目搜索。
更稳妥的做法是把“关闭”“硬边”“模糊”作为不同语义。下面的函数只负责组件shadow,不负责文字阴影。硬边在旧行为下不能靠radius=0实现,因此返回unsupported,让调用方选择替代设计,而不是偷偷改成0.01并宣称等价。
type ShadowIntent =
| { kind: 'none' }
| { kind: 'hard'; x: number; y: number }
| { kind: 'soft'; radius: number; x: number; y: number };
type ShadowPlan =
| { supported: false; reason: string }
| { supported: true; radius: number; offsetX: number; offsetY: number };
function componentShadow(intent: ShadowIntent, api26Behavior: boolean): ShadowPlan {
if (intent.kind === 'none') return { supported:true, radius:-1, offsetX:0, offsetY:0 };
if (!Number.isFinite(intent.x) || !Number.isFinite(intent.y)) throw new Error('invalid offset');
if (intent.kind === 'hard') {
if (!api26Behavior) return { supported:false, reason:'hard shadow requires the new behavior' };
return { supported:true, radius:0, offsetX:intent.x, offsetY:intent.y };
}
if (!Number.isFinite(intent.radius) || intent.radius <= 0) throw new Error('soft radius must be positive');
return { supported:true, radius:intent.radius, offsetX:intent.x, offsetY:intent.y };
}
function check(value: boolean): void { if (!value) throw new Error('assertion failed'); }
const noneOld = componentShadow({kind:'none'},false);
const noneNew = componentShadow({kind:'none'},true);
check(noneOld.supported && noneOld.radius < 0);
check(noneNew.supported && noneNew.radius < 0);
check(!componentShadow({kind:'hard',x:12,y:12},false).supported);
const hard = componentShadow({kind:'hard',x:12,y:12},true);
check(hard.supported && hard.radius === 0);
const soft = componentShadow({kind:'soft',radius:8,x:2,y:4},true);
check(soft.supported && soft.radius === 8);
let rejected = false;
try { componentShadow({kind:'soft',radius:0,x:0,y:0},true); } catch { rejected = true; }
check(rejected);
api26Behavior是示例注入的环境事实,不是系统API。正式工程应由构建目标和运行环境支持情况确定,不要只根据一个业务开关假装旧设备拥有新行为。这个封装的价值在于把意图变成可测试的数据,而不是提供新的平台兼容魔法。
案例二:有意保留硬阴影的按钮
海报式工具界面可能需要清晰的偏移轮廓,没有模糊并不是缺陷。对于这种组件,批量把radius:0改成-1反而会消掉设计效果。
迁移记录至少包含组件位置、属性类型、原始意图三个字段。下面的审查函数故意只生成建议,不直接修改源代码。无法确定意图的条目进入人工检查,而不是猜测。
type ShadowUse = { api:'shadow'|'textShadow'; radius:number; intent:'none'|'hard'|'unknown' };
function migrationAdvice(use: ShadowUse): string {
if (use.api === 'textShadow') return 'leave-text-shadow-unchanged';
if (use.radius !== 0) return 'not-a-zero-radius-migration';
if (use.intent === 'none') return 'replace-zero-with-negative';
if (use.intent === 'hard') return 'keep-zero-and-check-offset';
return 'review-design-intent';
}
check(migrationAdvice({api:'shadow',radius:0,intent:'none'}) === 'replace-zero-with-negative');
check(migrationAdvice({api:'shadow',radius:0,intent:'hard'}) === 'keep-zero-and-check-offset');
check(migrationAdvice({api:'textShadow',radius:0,intent:'none'}) === 'leave-text-shadow-unchanged');
check(migrationAdvice({api:'shadow',radius:0,intent:'unknown'}) === 'review-design-intent');
官方说明明确指出Text组件的textShadow不受此次变更影响。这就是不能把搜索结果中的所有“radius: 0”一键替换的原因。radius还可能属于圆角、模糊、几何计算,连属性所在的上下文都不看,改动范围会远大于问题本身。
方案怎么选
只有一处旧按钮,直接在原组件将用于关闭的0改成负数,改动最小。有统一设计系统,则应改语义令牌并保留映射测试。需要硬阴影的设计,明确标成hard,同时为旧行为提供单独视觉方案。这三种处理各有场景,不需要为了两行样式强行搭一个复杂兼容框架。
动画也应纳入检查。如果半径从正值逐渐变化到0,结束状态可能从“消失”变成“硬边”。不要仅修静态默认值而遗漏交互态、禁用态、主题切换和动画终点。具体视觉结果要用真实页面验证,本文没有测量动画性能。
上线前在同一张测试页面检查负数、零、正数以及零加偏移四组;分别记录target设置和系统版本;再检查Text.textShadow没有被无关改动。最后比较组件默认态与按压态。这样留下的证据能直接解释变化来自哪里,而不是把所有截图差异归咎于“新系统样式变了”。
这次迁移真正值得保留的不是一个负数,而是把“关闭效果”与“效果强度为零”分开表达的习惯。语义明确,版本变化时才知道哪些值该改,哪些必须保留。
官方资料
更多推荐

所有评论(0)