ArkUI为组件提供了通用的属性动画和转场动画能力的同时,还为一些组件提供了默认的动画效果。例如,List的滑动动效,Button的点击动效,是组件自带的默认动画效果。在组件默认动画效果的基础上,开发者还可以通过属性动画和转场动画对容器组件内的子组件动效进行定制。
使用组件默认动画
组件默认动效具备以下功能:
- 提示用户当前状态,例如用户点击Button组件时,Button组件默认变灰,用户即确定完成选中操作。
- 提升界面精致程度和生动性。
- 减少开发者工作量,例如列表滑动组件自带滑动动效,开发者直接调用即可。
更多效果,可以参考组件说明。
示例代码和效果如下。
@Entry
@Component
struct ComponentDemo {
build() {
Row() {
Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
.select(true)
.shape(CheckBoxShape.CIRCLE)
.size({ width: 50, height: 50 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
ts
打造组件定制化动效
部分组件支持通过属性动画和转场动画自定义组件子Item的动效,实现定制化动画效果。例如,Scroll组件中可对各个子组件在滑动时的动画效果进行定制。
- 在滑动或者点击操作时通过改变各个Scroll子组件的仿射属性来实现各种效果。
- 如果要在滑动过程中定制动效,可在滑动回调onScroll中监控滑动距离,并计算每个组件的仿射属性。也可以自己定义手势,通过手势监控位置,手动调用ScrollTo改变滑动位置。
- 在滑动回调onScrollStop或手势结束回调中对滑动的最终位置进行微调。
定制Scroll组件滑动动效示例代码和效果如下。
import curves from '@ohos.curves';
import window from '@ohos.window';
import display from '@ohos.display';
import mediaquery from '@ohos.mediaquery';
import UIAbility from '@ohos.app.ability.UIAbility';
export default class GlobalContext extends AppStorage{
static mainWin: window.Window|undefined = undefined;
static mainWindowSize:window.Size|undefined = undefined;
}
/**
* 窗口、屏幕相关信息管理类
*/
export class WindowManager {
private static instance: WindowManager|null = null;
private displayInfo: display.Display|null = null;
private orientationListener = mediaquery.matchMediaSync('(orientation: landscape)');
constructor() {
this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) = > { this.onPortrait(mediaQueryResult) })
this.loadDisplayInfo()
}
/**
* 设置主window窗口
* @param win 当前app窗口
*/
setMainWin(win: window.Window) {
if (win == null) {
return
}
GlobalContext.mainWin = win;
win.on("windowSizeChange", (data: window.Size) = > {
if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) {
GlobalContext.mainWindowSize = data;
} else {
if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) {
return
}
GlobalContext.mainWindowSize = data;
}
let winWidth = this.getMainWindowWidth();
AppStorage.setOrCreate< number >('mainWinWidth', winWidth)
let winHeight = this.getMainWindowHeight();
AppStorage.setOrCreate< number >('mainWinHeight', winHeight)
let context:UIAbility = new UIAbility()
context.context.eventHub.emit("windowSizeChange", winWidth, winHeight)
})
}
static getInstance(): WindowManager {
if (WindowManager.instance == null) {
WindowManager.instance = new WindowManager();
}
return WindowManager.instance
}
private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
if (mediaQueryResult.matches == AppStorage.get< boolean >('isLandscape')) {
return
}
AppStorage.setOrCreate< boolean >('isLandscape', mediaQueryResult.matches)
this.loadDisplayInfo()
}
/**
* 切换屏幕方向
* @param ori 常量枚举值:window.Orientation
*/
changeOrientation(ori: window.Orientation) {
if (GlobalContext.mainWin != null) {
GlobalContext.mainWin.setPreferredOrientation(ori)
}
}
private loadDisplayInfo() {
this.displayInfo = display.getDefaultDisplaySync()
AppStorage.setOrCreate< number >('displayWidth', this.getDisplayWidth())
AppStorage.setOrCreate< number >('displayHeight', this.getDisplayHeight())
}
/**
* 获取main窗口宽度,单位vp
*/
getMainWindowWidth(): number {
return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.width) : 0
}
/**
* 获取main窗口高度,单位vp
*/
getMainWindowHeight(): number {
return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.height) : 0
}
/**
* 获取屏幕宽度,单位vp
*/
getDisplayWidth(): number {
return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0
}
/**
* 获取屏幕高度,单位vp
*/
getDisplayHeight(): number {
return this.displayInfo != null ? px2vp(this.displayInfo.height) : 0
}
/**
* 释放资源
*/
release() {
if (this.orientationListener) {
this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) = > { this.onPortrait(mediaQueryResult)})
}
if (GlobalContext.mainWin != null) {
GlobalContext.mainWin.off('windowSizeChange')
}
WindowManager.instance = null;
}
}
/**
* 封装任务卡片信息数据类
*/
export class TaskData {
bgColor: Color | string | Resource = Color.White;
index: number = 0;
taskInfo: string = 'music';
constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) {
this.bgColor = bgColor;
this.index = index;
this.taskInfo = taskInfo;
}
}
export const taskDataArr: Array< TaskData > =
[
new TaskData('#317AF7', 0, 'music'),
new TaskData('#D94838', 1, 'mall'),
new TaskData('#DB6B42 ', 2, 'photos'),
new TaskData('#5BA854', 3, 'setting'),
new TaskData('#317AF7', 4, 'call'),
new TaskData('#D94838', 5, 'music'),
new TaskData('#DB6B42', 6, 'mall'),
new TaskData('#5BA854', 7, 'photos'),
new TaskData('#D94838', 8, 'setting'),
new TaskData('#DB6B42', 9, 'call'),
new TaskData('#5BA854', 10, 'music')
];
@Entry
@Component
export struct TaskSwitchMainPage {
displayWidth: number = WindowManager.getInstance().getDisplayWidth();
scroller: Scroller = new Scroller();
cardSpace: number = 0; // 卡片间距
cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // 卡片宽度
cardHeight: number = 400; // 卡片高度
cardPosition: Array< number > = []; // 卡片初始位置
clickIndex: boolean = false;
@State taskViewOffsetX: number = 0;
@State cardOffset: number = this.displayWidth / 4;
lastCardOffset: number = this.cardOffset;
startTime: number|undefined=undefined
// 每个卡片初始位置
aboutToAppear() {
for (let i = 0; i < taskDataArr.length; i++) {
this.cardPosition[i] = i * (this.cardWidth + this.cardSpace);
}
}
// 每个卡片位置
getProgress(index: number): number {
let progress = (this.cardOffset + this.cardPosition[index] - this.taskViewOffsetX + this.cardWidth / 2) / this.displayWidth;
return progress
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
// 背景
Column()
.width('100%')
.height('100%')
.backgroundColor(0xF0F0F0)
// 滑动组件
Scroll(this.scroller) {
Row({ space: this.cardSpace }) {
ForEach(taskDataArr, (item:TaskData, index) = > {
Column()
.width(this.cardWidth)
.height(this.cardHeight)
.backgroundColor(item.bgColor)
.borderStyle(BorderStyle.Solid)
.borderWidth(1)
.borderColor(0xAFEEEE)
.borderRadius(15)
// 计算子组件的仿射属性
.scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ?
{
x: 1.1 - Math.abs(0.5 - this.getProgress(index)),
y: 1.1 - Math.abs(0.5 - this.getProgress(index))
} :
{ x: 1, y: 1 })
.animation({ curve: Curve.Smooth })
// 滑动动画
.translate({ x: this.cardOffset })
.animation({ curve: curves.springMotion() })
.zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1)
}, (item:TaskData) = > item.toString())
}
.width((this.cardWidth + this.cardSpace) * (taskDataArr.length + 1))
.height('100%')
}
.gesture(
GestureGroup(GestureMode.Parallel,
PanGesture({ direction: PanDirection.Horizontal, distance: 5 })
.onActionStart((event: GestureEvent|undefined) = > {
if(event){
this.startTime = event.timestamp;
}
})
.onActionUpdate((event: GestureEvent|undefined) = > {
if(event){
this.cardOffset = this.lastCardOffset + event.offsetX;
}
})
.onActionEnd((event: GestureEvent|undefined) = > {
if(event){
let time = 0
if(this.startTime){
time = event.timestamp - this.startTime;
}
let speed = event.offsetX / (time / 1000000000);
let moveX = Math.pow(speed, 2) / 7000 * (speed > 0 ? 1 : -1);
this.cardOffset += moveX;
// 左滑大于最右侧位置
let cardOffsetMax = -(taskDataArr.length - 1) * (this.displayWidth / 2);
if (this.cardOffset < cardOffsetMax) {
this.cardOffset = cardOffsetMax;
}
// 右滑大于最左侧位置
if (this.cardOffset > this.displayWidth / 4) {
this.cardOffset = this.displayWidth / 4;
}
// 左右滑动距离不满足/满足切换关系时,补位/退回
let remainMargin = this.cardOffset % (this.displayWidth / 2);
if (remainMargin < 0) {
remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2;
}
if (remainMargin <= this.displayWidth / 4) {
this.cardOffset += this.displayWidth / 4 - remainMargin;
} else {
this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin);
}
// 记录本次滑动偏移量
this.lastCardOffset = this.cardOffset;
}
})
), GestureMask.IgnoreInternal)
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
// 滑动到首尾位置
Button('Move to first/last')
.backgroundColor(0x888888)
.margin({ bottom: 30 })
.onClick(() = > {
this.clickIndex = !this.clickIndex;
if (this.clickIndex) {
this.cardOffset = this.displayWidth / 4;
} else {
this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) * this.displayWidth / 2;
}
this.lastCardOffset = this.cardOffset;
})
}
.width('100%')
.height('100%')
}
}
ts
审核编辑 黄宇
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。
举报投诉
-
组件
+关注
关注
1文章
506浏览量
17809 -
鸿蒙
+关注
关注
57文章
2325浏览量
42766 -
OpenHarmony
+关注
关注
25文章
3682浏览量
16183
发布评论请先 登录
相关推荐
OpenHarmony实战开发-如何实现动画帧
你们的 『点赞和评论』,才是我创造的动力。
关注小编,同时可以期待后续文章ing?,不定期分享原创知识。
更多鸿蒙最新技术知识点,请关注作者博客:鸿蒙实战经验分享:鸿蒙基础入门开发宝典! (qq.com)**
发表于 05-06 14:11
OpenHarmony实战开发-如何实现窗口开发概述
操作系统而言,窗口模块提供了不同应用界面的组织管理逻辑。
窗口模块的用途
在OpenHarmony中,窗口模块主要负责以下职责:
提供应用和系统界面的窗口对象。 应用开发者通过窗口加载UI界面,实现界面
发表于 05-06 14:29
HarmonyOS Lottie组件,让动画绘制更简单
了丰富的API,让开发者能轻松控制动画,大大提高了开发效率。
二、Lottie实战
通过上文对Lottie的介绍,相信很多小伙伴已经感受到了Lottie
发表于 02-22 14:55
OpenHarmony标准设备应用开发笔记汇总
如何在标准设备上运行一个最简单的 OpenHarmony 程序。2、如何在OpenHarmony中实现音乐的播放本章是 OpenHarmony 标准设备应用
发表于 03-28 14:19
OpenHarmony标准设备应用开发(二)——布局、动画与音乐
节的基础上,学到更多 ArkUI 组件和布局在 OpenHarmony 中的应用,以及如何在自己的应用中实现显示动画的效果。代码链接:https://gitee.com/
发表于 04-07 17:09
HarmonyOS/OpenHarmony应用开发-属性动画
组件的某些通用属性变化时,可以通过属性动画实现渐变过渡效果,提升用户体验。支持的属性包括width、height、backgroundColor、opacity、scale、rotate
发表于 01-03 10:51
OpenHarmony应用开发—ArkUI组件集合
页面
接口参考:@ohos.curves, @ohos.router
显示动画
用到全局组件TitleBar,IntroductionTitle实现页面
接口参考:animateTo
属性
发表于 09-22 14:56
基于openharmony适配移植的多种动画效果实现教程
项目介绍 项目名称: OhosLoadingAnimation 所属系列:openharmony的第三方组件适配移植 功能:实现多种动画效果 项目移植状态:主功能完成 调用差异
发表于 04-02 10:55
•0次下载
openharmony第三方组件适配移植的动画库实现
项目介绍 项目名称:CanAnimation 所属系列:openharmony的第三方组件适配移植 功能:使用openharmony属性动画写的一个库,可组建
发表于 04-02 11:30
•3次下载
openharmony第三方组件适配移植的SVGA动画渲染库
项目介绍 项目名称:SVGAPlayer-Ohos 所属系列:openharmony的第三方组件适配移植 功能:SVGAPlayer-Ohos 是一个轻量的动画渲染库。你可以使用工具从 Adobe
发表于 04-02 11:47
•15次下载
OpenHarmony轻量系统书籍推荐《OpenHarmony轻量设备开发理论与实战》
最近大家问的智能家居套件方面有没有可以参考的资料,这里给大家统一回复一下 推荐大家可以看这本书 《OpenHarmony轻量设备开发理论与实战》 本书系统地讲授OpenHarmony
评论