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没有被无关改动。最后比较组件默认态与按压态。这样留下的证据能直接解释变化来自哪里,而不是把所有截图差异归咎于“新系统样式变了”。

这次迁移真正值得保留的不是一个负数,而是把“关闭效果”与“效果强度为零”分开表达的习惯。语义明确,版本变化时才知道哪些值该改,哪些必须保留。

官方资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐