HarmonyOS6 PC 开发实战:摇晃动画——用代码模拟物理抖动效果

张开发
2026/6/10 3:59:47 15 分钟阅读

分享文章

HarmonyOS6 PC 开发实战:摇晃动画——用代码模拟物理抖动效果
登录表单输错密码输入框左右抖几下——这个效果你应该见过无数次了。但你可能没想过这个抖几下背后其实挺有讲究的。我最早实现摇晃动画的时候想当然地写了一个左-右-左-右的等幅振荡。效果出来后怎么看都不对劲像一个机器在做机械运动完全没有抖的感觉。后来去翻了一些物理动画的资料才明白——真实的摇晃是一个振幅逐渐衰减的过程。就像你推了一下桌上的水杯它晃几下就停下来了每一下比上一下幅度小。今天我们就来在HarmonyOS6 PC端实现一个物理感十足的摇晃动画并且支持三种不同强度。效果预览页面上有一个红色的圆角矩形上面放着一个感叹号!。下面有三个按钮轻微摇晃小幅度抖动像被轻轻碰了一下标准摇晃中等幅度表单验证失败的经典效果剧烈摇晃大幅度抖动像出了严重错误三种强度的摇晃使用相同的衰减序列但基础振幅不同。摇晃结束后元素自动回到原位。核心设计振幅递减序列摇晃动画的灵魂就是这个数组constpattern[-1,1,-1,1,-0.8,0.8,-0.5,0.5,-0.2,0.2,0]这个序列模拟了一个真实的物理衰减过程。我们来分析一下序号值含义0-1向左最大偏移11向右最大偏移2-1向左最大偏移31向右最大偏移4-0.8向左偏移振幅开始衰减50.8向右偏移6-0.5向左偏移继续衰减70.5向右偏移8-0.2向左偏移接近静止90.2向右偏移100回到中心你看前四次是满幅度振荡-1和1然后逐步衰减到0.8、0.5、0.2最后归零。这个过程模拟的就是阻尼振动——初始能量大但随着摩擦/阻尼消耗能量振幅越来越小。如果你改成等幅振荡比如[-1, 1, -1, 1, -1, 1, 0]效果就会像电动牙刷——机械、生硬、不自然。衰减才是摇晃动画的关键。状态变量EntryComponentstruct ShakeDemo{StateshakeX:number0StateshakeIntensity:number1// ...}两个状态变量shakeX当前帧的偏移基准值取pattern数组中的值-1到1之间shakeIntensity摇晃强度系数0.5为轻微1为标准1.5为剧烈实际的像素偏移量通过公式计算实际偏移 shakeX × shakeIntensity × 10所以标准摇晃intensity1的最大偏移是 ±10px轻微摇晃intensity0.5是 ±5px剧烈摇晃intensity1.5是 ±15px。这个设计很巧妙——用同一个衰减序列通过乘以不同的强度系数就实现了三种摇晃效果。不需要写三组不同的序列。摇晃执行逻辑核心的摇晃函数_doShake是这样的_doShake(){constpattern[-1,1,-1,1,-0.8,0.8,-0.5,0.5,-0.2,0.2,0]leti0conststep(){if(ipattern.length){this.shakeXpattern[i]isetTimeout(step,50)}else{this.shakeX0}}step()}逻辑很直白定义衰减序列pattern用递归setTimeout每50ms执行一步每一步把shakeX设为序列中对应的值序列跑完后确保shakeX回到0为什么是50ms50ms的步进时间意味着每帧之间的间隔是50ms对应20fps的帧率。对于摇晃这种快速抖动效果来说这个帧率是合适的。为什么不用更高帧率因为摇晃动画的本质是在离散位置之间跳变它不是平滑过渡——每一帧元素突然出现在一个新位置模拟的就是物理振动中那种快速来回的感觉。如果帧率太高比如60fps反而会变成平滑的来回移动失去抖的质感。如果你想要更柔和的摇晃效果可以把步进时间改成60ms或70ms。如果想要更急促的效果改成30ms或40ms。整个摇晃过程的总时长11帧 × 50ms 550ms。大概半秒的摇晃时间这跟你在各种应用中见到的错误提示抖动时长基本一致。太短了来不及抖出衰减感太长了又显得拖沓。500-600ms是个非常舒适的区间。按钮触发强度控制三个按钮的点击逻辑非常简单Row({space:10}){Button(轻微摇晃).onClick((){this.shakeIntensity0.5this._doShake()})Button(标准摇晃).onClick((){this.shakeIntensity1this._doShake()})Button(剧烈摇晃).onClick((){this.shakeIntensity1.5this._doShake()})}.width(100%).justifyContent(FlexAlign.SpaceEvenly).margin({top:12})先设置强度系数再调用_doShake()。因为_doShake内部读取的是this.shakeIntensity的当前值所以设置和调用的顺序很重要——必须先设强度再触发摇晃。视觉元素的动画绑定被摇晃的元素通过.translate()和.animation()来响应状态变化Column().width(100).height(60).backgroundColor(#FF6B6B).borderRadius(12).translate({x:this.shakeX*this.shakeIntensity*10}).animation({duration:50,curve:Curve.Linear})几个值得注意的点.translate({ x: ... })把水平偏移量绑定到一个计算表达式上。每当shakeX变化偏移量跟着变。.animation({ duration: 50, curve: Curve.Linear })这个50ms的动画时长正好等于步进间隔。也就是说每一帧的偏移变化会在50ms内用线性插值完成——刚好在下一帧开始时完成过渡。这样视觉上就是连续的平滑移动而不是突然跳变。如果你把.animation()的duration设成0就会变成纯跳变——每一帧元素瞬间出现在新位置。对于摇晃动画来说带50ms的线性过渡效果更好视觉上更顺滑。完整代码EntryComponentstruct ShakeDemo{StateshakeX:number0StateshakeIntensity:number1build(){Column(){Scroll(){Column(){Text(摇晃动画).fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:8})Column(){// 摇晃目标Row(){Column().width(100).height(60).backgroundColor(#FF6B6B).borderRadius(12).translate({x:this.shakeX*this.shakeIntensity*10}).animation({duration:50,curve:Curve.Linear})Column(){Text(!).fontSize(30).fontColor(#FFFFFF).fontWeight(FontWeight.Bold)}.width(100%).height(100%).position({x:0,y:0}).justifyContent(FlexAlign.Center)}.position({x:0,y:0}).width(100).height(60)Row().height(80)// 控制按钮Row({space:10}){Button(轻微摇晃).onClick((){this.shakeIntensity0.5this._doShake()})Button(标准摇晃).onClick((){this.shakeIntensity1this._doShake()})Button(剧烈摇晃).onClick((){this.shakeIntensity1.5this._doShake()})}.width(100%).justifyContent(FlexAlign.SpaceEvenly).margin({top:12})}.width(100%).backgroundColor(#FFFFFF).borderRadius(12).padding(16).alignItems(HorizontalAlign.Center)}.width(100%)}.layoutWeight(1)}.width(100%).height(100%).backgroundColor(#F5F6FA).padding(16)}_doShake(){constpattern[-1,1,-1,1,-0.8,0.8,-0.5,0.5,-0.2,0.2,0]leti0conststep(){if(ipattern.length){this.shakeXpattern[i]isetTimeout(step,50)}else{this.shakeX0}}step()}}摇晃动画在PC端的实际应用场景摇晃动画在PC端的使用场景比大家想象的要多。表单验证失败这是最经典的场景。用户输错了密码、填了不合法的邮箱、漏了必填项——输入框左右抖几下比任何红色文字都更能引起注意。在PC端的表单场景里摇晃动画有一个隐藏的好处PC用户通常会同时开多个窗口注意力不一定在当前应用上。一个摇晃动画能有效地把用户的视线拉回来。// 模拟表单验证场景Button(登录).onClick((){if(password.length6){this.shakeIntensity1this._doShake()this.errorText密码不能少于6位}})错误提示/警告框不只是输入框整个错误提示卡片也可以做摇晃效果。比如一个弹出的错误通知出现的时候先左右抖一下然后再显示错误详情。这比直接弹出来更有警告的意味。操作失败反馈拖拽文件到不支持的区域、尝试删除不可删除的项目、点击灰色的禁用按钮——这些操作被拒绝的场景都可以用摇晃来反馈。比起冷冰冰的弹窗提示摇晃动画更温和也更直觉。删除确认用户点击删除按钮时被删除的项目先摇晃一下给用户一个你确定吗的暗示。如果用户再次点击确认才真正删除。这种交互比传统的确认对话框更轻量。衰减动画序列的设计方法摇晃动画的核心——振幅递减序列——其实是一种通用的动画设计模式。掌握了这个模式你可以用它来做很多不同的效果。弹性落地效果一个元素从高处落下着地后弹跳几次每次弹起的高度递减constbouncePattern[0,-50,0,-30,0,-15,0,-5,0]// 负值表示向上弹起0表示地面果冻晃动效果一个元素被点击后像果冻一样晃动幅度逐渐减小constjellyPattern[1.2,0.8,1.1,0.9,1.05,0.95,1]// 这些是缩放因子1是正常大小弹簧回弹效果一个滑块被拖拽后松手回到原位的过程中有过冲和回弹constspringPattern[1,-0.3,0.15,-0.05,0]// 1是起点被拖到的位置0是终点原位中间是过冲和回弹这些效果的共同点是不是简单的从A到B的线性运动而是有一个振荡衰减的过程。这种运动模式更接近物理世界所以看起来更自然、更舒服。设计衰减序列的两个原则交替符号。正负交替才能产生来回的感觉。如果符号不交替就只是单方向的缓动。递减幅度。每个周期的峰值应该比前一个小。衰减比率不必是等比的——前几次可以衰减得慢一些保持动能后面衰减得快一些快速收敛。这样更接近真实的阻尼振动。踩坑记录坑1摇晃结束后元素没有回到原位如果你忘了在pattern数组末尾加0或者在循环结束后没有手动设置this.shakeX 0元素就会停在一个偏移位置上。一定要确保最终状态是shakeX 0。坑2快速连续点击导致动画叠加如果用户疯狂点击标准摇晃按钮每次点击都会启动一个新的_doShake调用。多个摇晃序列同时修改shakeX视觉上会非常混乱。解决方案加一个正在摇晃的锁。StateisShaking:booleanfalse_doShake(){if(this.isShaking)returnthis.isShakingtrueconstpattern[-1,1,-1,1,-0.8,0.8,-0.5,0.5,-0.2,0.2,0]leti0conststep(){if(ipattern.length){this.shakeXpattern[i]isetTimeout(step,50)}else{this.shakeX0this.isShakingfalse}}step()}坑3.animation()的 duration 跟步进时间不匹配如果.animation()的duration远大于步进时间比如设了200ms每一帧的过渡还没完成就被下一帧覆盖了视觉上会出现拖影的效果。建议duration等于或略小于步进时间。扩展用 animateTo 替代逐帧控制如果你觉得逐帧控制太底层也可以用animateTo链来实现类似的效果。思路是把每一帧的偏移作为一次animateTo调用_doShakeAnimated(){constpattern[-1,1,-1,1,-0.8,0.8,-0.5,0.5,-0.2,0.2,0]leti0constdoStep(){if(ipattern.length)returnanimateTo({duration:50,curve:Curve.Linear},(){this.shakeXpattern[i]})isetTimeout(doStep,50)}doStep()}两种方式的最终效果差不多但animateTo方式更容易跟其他动画效果组合——比如你想在摇晃的同时做一个短暂的缩放或颜色变化在animateTo闭包里加一行就行了。小结摇晃动画看起来简单但它背后的振幅递减序列是个非常有价值的动画设计模式。核心要点pattern数组定义了摇晃的运动轨迹正负交替幅度递减递归setTimeout每50ms走一步550ms完成整个摇晃shakeIntensity系数控制摇晃强度同一个序列实现多种效果.translate().animation()负责视觉呈现在HarmonyOS6 PC端的表单验证、错误提示、操作反馈这些场景中摇晃动画都是非常好的选择。它比弹窗更轻量比纯文字提示更有存在感是一个恰到好处的交互反馈方式。

更多文章