文章目录
水波动画
- 场景描述
在本场景中,圆形按钮上会依次出现多个水波状圆环,这些圆环从中心向外进行扩散,进而凸显功能,实现效果如图所示。
图7 使用显式动画实现水波纹动效

- 实现原理
水波圆环以圆形按钮为中心,将多个圆形图层逐渐向外扩展放大,每个圆形图层的动画开始时间稍微错开,进而形成多个水波圆环依次扩散的效果。其动效实现步骤如下。
- 实现圆形图层,以圆形图层作为水波圆环的基础形状,并设置相关背景属性。
- 通过显示动画animateTo实现圆形图层放大的动效,并设置延迟时间。
- 开发步骤
实现圆形图层,通过Stack将圆形图层与Button组件进行重叠。
typescript
@Component
struct ButtonWithWaterRipples {
@Link isListening: boolean;
@State immediatelyOpacity: number = 0.5;
@State immediatelyScale: Scale = { x: 1, y: 1 };
@State delayOpacity: number = 0.5;
@State delayScale: Scale = { x: 1, y: 1 };
private readonly BUTTON_SIZE: number = 120;
private readonly BUTTON_CLICK_SCALE: number = 0.8;
private readonly ANIMATION_DURATION: number = 1300;
@Styles
ripplesStyle() {
.width(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE)
.height(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE)
.borderRadius(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE / 2)
.backgroundColor(Color.Red)
}
build() {
Stack() {
Stack()
.ripplesStyle()
.opacity(this.immediatelyOpacity)
.scale(this.immediatelyScale)
Stack()
.ripplesStyle()
.opacity(this.delayOpacity)
.scale(this.delayScale)
Button() {
Image($r('app.media.ic_public_music_filled'))
.width($r('app.float.water_ripples_width'))
.fillColor(Color.White)
}
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: this.BUTTON_CLICK_SCALE })
.backgroundColor($r('app.color.music_icon'))
.type(ButtonType.Circle)
.width(this.BUTTON_SIZE)
.height(this.BUTTON_SIZE)
.zIndex(1)
// ...
}
}
}
代码逻辑走读:
- 组件定义与状态初始化 :
- 定义了一个
ButtonWithWaterRipples组件,并初始化了几个状态变量,如immediatelyOpacity、immediatelyScale、delayOpacity和delayScale,用于控制水波纹的透明度和缩放。
- 定义了一个
- 样式定义 :
- 定义了一个
ripplesStyle方法,用于设置水波纹的样式,包括宽度、高度、边框半径和背景颜色。
- 定义了一个
- 构建UI结构 :
- 使用
Stack布局来构建UI,内部包含两个Stack,分别用于显示立即的水波纹和延迟的水波纹。 - 每个
Stack应用了ripplesStyle样式,并根据状态变量设置了透明度和缩放比例。
- 使用
- 按钮控件 :
- 在
Stack布局中嵌套了一个Button控件,按钮内部包含一个Image,用于显示按钮的图标。 - 按钮设置了点击效果,包括点击级别和缩放比例。
- 按钮的背景颜色和类型也进行了设置。
- 在
- 水波纹效果实现 :
- 通过状态变量的变化,实现了水波纹的淡入淡出和缩放效果,当按钮被点击时,水波纹的透明度和缩放比例会发生变化,从而产生水波纹效果。
实现圆形图层的放大动效,并设置延迟时间。
typescript
Button() {
Image($r('app.media.ic_public_music_filled'))
.width($r('app.float.water_ripples_width'))
.fillColor(Color.White)
}
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: this.BUTTON_CLICK_SCALE })
.backgroundColor($r('app.color.music_icon'))
.type(ButtonType.Circle)
.width(this.BUTTON_SIZE)
.height(this.BUTTON_SIZE)
.zIndex(1)
.onClick(() => {
this.isListening = !this.isListening;
if (this.isListening) {
this.getUIContext().animateTo({
duration: this.ANIMATION_DURATION,
iterations: CommonConstants.ITERATIONS,
curve: Curve.EaseInOut
}, () => {
this.immediatelyOpacity = CommonConstants.COMMON_NUMBER_0;
this.immediatelyScale = {
x: CommonConstants.COMMON_NUMBER_6,
y: CommonConstants.COMMON_NUMBER_6
};
})
this.getUIContext().animateTo({
duration: this.ANIMATION_DURATION,
iterations: CommonConstants.ITERATIONS,
curve: Curve.EaseInOut,
delay: CommonConstants.DELAY_200
}, () => {
this.delayOpacity = CommonConstants.COMMON_NUMBER_0;
this.delayScale = {
x: CommonConstants.COMMON_NUMBER_6,
y: CommonConstants.COMMON_NUMBER_6
};
})
} else {
// Break the animation by modifying the variable with a closure of duration 0.
this.getUIContext().animateTo({ duration: CommonConstants.COMMON_NUMBER_0 }, () => {
this.immediatelyOpacity = CommonConstants.COMMON_NUMBER;
this.delayOpacity = CommonConstants.COMMON_NUMBER;
this.immediatelyScale = {
x: CommonConstants.COMMON_NUMBER_1,
y: CommonConstants.COMMON_NUMBER_1
};
this.delayScale = {
x: CommonConstants.COMMON_NUMBER_1,
y: CommonConstants.COMMON_NUMBER_1
};
})
}
})
代码逻辑走读:
- 按钮定义 :
- 使用
Button()创建一个按钮组件。 - 按钮内部包含一个
Image组件,显示音乐图标。
- 使用
- 图像设置 :
- 图像的宽度通过
$r('app.float.water_ripples_width')获取。 - 图像的填充颜色设置为白色。
- 图像的宽度通过
- 点击效果 :
- 设置按钮的点击效果为
ClickEffectLevel.HEAVY,并且缩放比例为this.BUTTON_CLICK_SCALE。
- 设置按钮的点击效果为
- 背景颜色 :
- 按钮的背景颜色通过
$r('app.color.music_icon')设置。
- 按钮的背景颜色通过
- 按钮类型和尺寸 :
- 按钮类型设置为
ButtonType.Circle。 - 按钮的宽度和高度都设置为
this.BUTTON_SIZE。 - 按钮的
zIndex设置为 1。
- 按钮类型设置为
- 点击事件处理 :
- 当按钮被点击时,
this.isListening状态会被切换。 - 如果this.isListening为真,按钮会启动两个动画:
- 第一个动画立即改变按钮的透明度和缩放比例。
- 第二个动画在 200 毫秒延迟后继续改变按钮的透明度和缩放比例。
- 如果
this.isListening为假,按钮会中断动画并恢复到初始状态。
- 当按钮被点击时,
微动画
- 场景描述
如图所示,在本场景中,在登录页面前需要勾选相关的协议,如果未勾选相关协议,提示框将会通过左右移动进行提示。
图8 使用关键帧动画实现左右移动提示

- 实现原理
提示框左右移动提醒是将提示框进行左移,然后再进行右移,如此往复循环多次。其动效可以分为提示框左移和提示框右移两段,可以使用keyframeAnimateTo接口实现分段的动画效果,实现步骤如下所示。
- 根据需要实现的动画效果,将动画拆分成若干个关键帧,即将动画进行分段,如本案例中将动画分成提示框左移和提示框右移两段。
- 根据不同的关键帧设置关键帧状态,即设置该段关键帧动画的持续时间、动画曲线和闭包函数等。
- 设置动画触发条件,使用通用事件点击、出现等,选择对应需求的触发方式。
- 开发步骤
通过keyframeAnimateTo来设置关键帧动画。
typescript
startAnimation() {
if (!this.uiContext) {
return;
}
this.translateX = CONFIGURATION.POSITION_ZERO;
this.uiContext.keyframeAnimateTo({ iterations: CONFIGURATION.PLAYBACK_COUNT }, [
{
duration: CONFIGURATION.ANIMATION_TIME,
event: () => {
this.translateX = CONFIGURATION.TRANSLATE_OFFSET_X;
}
},
{
duration: CONFIGURATION.ANIMATION_TIME,
event: () => {
this.translateX = CONFIGURATION.POSITION_ZERO;
}
}
]);
}
代码逻辑走读:
- 检查
uiContext是否存在 :- 如果
uiContext不存在,方法直接返回,不执行后续动画代码。
- 如果
- 初始化
translateX属性 :- 将
translateX属性设置为CONFIGURATION.POSITION_ZERO,作为动画起始位置。
- 将
- 调用
uiContext.keyframeAnimateTo方法 :- 该方法接受两个参数:动画配置(包括迭代次数)和关键帧数组。
- 动画配置中,
iterations设置为CONFIGURATION.PLAYBACK_COUNT,决定动画循环次数。 - 关键帧数组定义了动画的两个阶段:
- 第一个关键帧持续时间为
CONFIGURATION.ANIMATION_TIME,动画完成后,translateX被设置为CONFIGURATION.TRANSLATE_OFFSET_X。 - 第二个关键帧同样持续时间为
CONFIGURATION.ANIMATION_TIME,动画完成后,translateX被重置为CONFIGURATION.POSITION_ZERO。
- 第一个关键帧持续时间为
设置onClick事件,通过onClick事件调用关键帧动画。
typescript
Button($r('app.string.login_in'))
.type(ButtonType.Normal)
.borderRadius($r('app.integer.comm_border_radius'))
.fontColor($r('app.color.ohos_id_color_background'))
.fontSize($r('app.integer.login_button_font_size'))
.width(Constants.LAYOUT_MAX_PERCENT)
.onClick(() => {
if (!this.confirm) {
this.startVibrate();
this.startAnimation();
} else {
try {
this.getUIContext().getPromptAction().showToast({
message: $r('app.string.login_text')
});
} catch (err) {
let error = err as BusinessError;
hilog.error(0x0000, 'PageVibrateEffect', `error code=${error.code}, message=${error.message}`);
}
}
})
代码逻辑走读:
- 创建一个按钮组件,按钮文本通过
$r('app.string.login_in')获取资源字符串。 - 设置按钮类型为
Normal。 - 设置按钮的边框圆角半径,通过
$r('app.integer.comm_border_radius')获取资源整数。 - 设置按钮的字体颜色,通过
$r('app.color.ohos_id_color_background')获取资源颜色。 - 设置按钮的字体大小,通过
$r('app.integer.login_button_font_size')获取资源整数。 - 设置按钮的宽度为常量
Constants.LAYOUT_MAX_PERCENT。 - 为按钮添加点击事件处理函数:
- 如果
this.confirm为false,则调用this.startVibrate()和this.startAnimation()方法。 - 如果
this.confirm为true,则尝试显示一个提示消息。如果显示失败,捕获错误并记录错误信息。
- 如果
手势动画
- 场景描述
在本场景中,页面主要分为标题和列表两个部分,当向下滑动列表时,标题会跟随下滑手势扩展显示详细信息,其实现效果如下所示。
图9 使用属性动画实现手势动效

- 实现原理
在实现下拉缩放详情中,主要包含了两个部分,分别是列表下拉的手势和下拉后标题和列表的动画,详细实现步骤如下。
- 处理手势事件:通过onTouch事件记录当前的触摸点,判断当前的手势是否为向上或向下滑动。
- 通过属性动画animation实现标题缩放的效果:当列表向上或向下滑动时,改变列表的高度,并通过属性动画进行平滑过度,从而实现标题区域缩放的效果。
- 实现标题内容的平移:在标题区域进行缩放的同时,标题的内容也会同步进行平移,从而实现标题部分整体缩放的效果。
- 开发步骤
实现手势事件方法。当手指按下时,触发TouchType.Down事件记录当前触碰的位置。手指按压态在屏幕上移动时,可以通过当前的位置与初始位置进行比较,判断手势是否为向上或向下滑动,进而变更列表的高度。
typescript
handleTouchEvent(event: TouchEvent): void {
switch (event.type) {
case TouchType.Down:
this.downY = event.touches[0].y;
this.lastMoveY = event.touches[0].y;
this.isMoving = true;
this.duration = Constants.ANIMATE_DURATION_DRAG;
break;
case TouchType.Move:
const delta = event.touches[0].y - this.lastMoveY;
this.offsetY = event.touches[0].y - this.downY;
if (delta < 0) {
this.heightValue = Constants.AREA_HEIGHT_BEFORE;
this.isExpanded = false;
this.atStart = false;
}
if (delta > 0 && this.atStart) {
this.animateToThrottle(() => {
this.heightValue = Constants.AREA_HEIGHT_AFTER;
this.isExpanded = true;
}, 1000);
}
this.lastMoveY = event.touches[0].y;
break;
case TouchType.Cancel:
this.isMoving = false;
this.duration = Constants.ANIMATE_DURATION_BACK;
break;
default:
break;
}
}
代码逻辑走读:
- 事件类型判断 :函数首先通过
switch语句判断触摸事件的类型。 - 按下事件处理 :
- 当事件类型为
TouchType.Down时,记录按下时的Y坐标downY和lastMoveY。 - 设置
isMoving为true,表示开始移动。 - 设置动画持续时间
duration为拖动动画的持续时间Constants.ANIMATE_DURATION_DRAG。
- 当事件类型为
- 移动事件处理 :
- 当事件类型为
TouchType.Move时,计算当前Y坐标与上一次移动的Y坐标差delta。 - 更新
offsetY为当前Y坐标减去按下时的Y坐标。 - 如果
delta小于0且atStart为false,则设置heightValue为Constants.AREA_HEIGHT_BEFORE,并将isExpanded设置为false,同时将atStart设置为false。 - 如果
delta大于0且atStart为true,则调用animateToThrottle函数,在1秒后将heightValue设置为Constants.AREA_HEIGHT_AFTER,并将isExpanded设置为true。 - 更新
lastMoveY为当前Y坐标。
- 当事件类型为
- 取消事件处理 :
- 当事件类型为
TouchType.Cancel时,设置isMoving为false,表示停止移动。 - 设置动画持续时间
duration为返回动画的持续时间Constants.ANIMATE_DURATION_BACK。
- 当事件类型为
- 默认情况 :
- 如果事件类型不属于上述任何一种,则不做任何处理。
为列表设置触摸事件onTouch和属性动画,实现标题区域缩放的效果。
typescript
Column() {
List({ space: Constants.SEARCH_MEMO_SPACE }) {
ListItem() {
Search({ placeholder: $r('app.string.search_placeholder') })
.width(Constants.LAYOUT_MAX_PERCENT)
.height(Constants.LAYOUT_EIGHT_PERCENT)
.backgroundColor(Color.White)
.enableKeyboardOnFocus(false)
}
LazyForEach(this.memoData, (item: MemoInfo) => {
ListItem() {
MemoItem({ memoItem: item })
}
}, (item: MemoInfo) => JSON.stringify(item))
}
.scrollBar(BarState.Off)
.margin({ left: $r('app.float.layout_10'), right: $r('app.float.layout_10') })
.width(Constants.LAYOUT_NINETY_PERCENT)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.onReachStart(() => {
this.atStart = true;
})
}
.width(Constants.LAYOUT_MAX_PERCENT)
.height(this.heightValue)
.animation({ duration: this.duration, curve: Curve.FastOutLinearIn })
.onTouch((event: TouchEvent) => this.handleTouchEvent(event))
代码逻辑走读:
- 组件结构 :
- 使用
Column组件作为根容器,用于垂直排列子组件。 - 在
Column中嵌套了一个List组件,用于展示列表项。
- 使用
- 列表项定义 :
List组件包含一个ListItem,内部嵌套了一个Search组件,用于搜索功能。Search组件设置了占位符文本、宽度、高度、背景颜色,并禁用了键盘弹出。
- 数据加载与展示 :
- 使用
LazyForEach组件动态加载并展示memoData中的备忘录信息。 - 每个备忘录信息通过
MemoItem组件展示。
- 使用
- 列表属性设置 :
List组件设置了滚动条隐藏、左右边距、宽度、展开安全区域等属性。- 当列表滚动到顶部时,设置
atStart为true。
- 组件属性设置 :
Column组件设置了宽度、高度、动画效果、触摸事件处理等属性。
通过animation属性动画实现标题内容平移,从而达到标题整体缩放的效果。
typescript
Column() {
Row() {
Text(!this.isExpanded ? $r('app.string.memo_title') : '')
.fontSize($r('app.float.init_title_font_size'))
Blank()
// Image($r('app.media.is_public_add'))
// .width($r('app.float.menu_pic_layout'))
// .height($r('app.float.menu_pic_layout'))
// Image($r('app.media.ic_public_more'))
// .width($r('app.float.menu_pic_layout'))
// .height($r('app.float.menu_pic_layout'))
// .margin({ left: $r('app.float.layout_8') })
}
.width(Constants.LAYOUT_MAX_PERCENT)
.padding($r('app.float.layout_25'))
.margin({ top: $r('app.float.layout_10') })
.alignItems(VerticalAlign.Center)
.translate(this.getMenuTranslateOptions())
.animation({ duration: this.duration, curve: Curve.FastOutLinearIn })
Column() {
Text($r('app.string.memo_title'))
.fontSize($r('app.float.expanded_title_font_size'))
Text($r('app.string.memo_counts'))
.fontSize($r('app.float.memo_counts_font_size'))
.fontColor(Color.Grey)
}
.width(Constants.LAYOUT_MAX_PERCENT)
.padding({ left: $r('app.float.layout_25') })
.margin({ top: $r('app.float.layout_10') })
.alignItems(HorizontalAlign.Start)
.translate(this.getTitleTranslateOptions())
.scale(this.getTitleScaleOptions())
.animation({ duration: this.duration, curve: Curve.FastOutLinearIn })
.transition({ type: TransitionType.Insert, translate: { y: Constants.TRANSLATE_Y } })
.visibility(this.isExpanded ? Visibility.Visible : Visibility.Hidden)
}
.height(Constants.LAYOUT_MAX_PERCENT)
.width(Constants.LAYOUT_MAX_PERCENT)
代码逻辑走读:
- 整体布局 :
- 使用
Column组件作为根布局,包含两个子布局,一个是Row,另一个是嵌套的Column。
- 使用
- 第一部分:Row布局 :
- 包含一个
Text组件,根据this.isExpanded的值显示或隐藏标题文本。 - 使用
Blank组件作为占位符,可能用于未来扩展。 Row组件设置了宽度、内边距、外边距、对齐方式以及动画效果,用于控制标题行的显示和位置。
- 包含一个
- 第二部分:嵌套的Column布局 :
- 包含两个
Text组件,分别显示扩展后的标题和副标题。 - 设置了宽度、内边距、外边距、对齐方式、缩放比例以及动画效果,用于控制详细信息的显示和位置。
- 使用
transition属性实现插入动画,当this.isExpanded为真时,显示该布局。 - 使用
visibility属性控制布局的可见性,根据this.isExpanded的值显示或隐藏。
- 包含两个
- 动画和状态控制 :
- 使用
animation和translate、scale等属性来实现平滑的过渡效果。
Row布局**: - 包含一个
Text组件,根据this.isExpanded的值显示或隐藏标题文本。 - 使用
Blank组件作为占位符,可能用于未来扩展。 Row组件设置了宽度、内边距、外边距、对齐方式以及动画效果,用于控制标题行的显示和位置。
- 使用