diff --git a/zh-cn/third-party-cases/Readme-CN.md b/zh-cn/third-party-cases/Readme-CN.md index 2040683fcf5461705069e1061d688217e13429d7..f222b4547bed38472a016da8ac6299920adea41d 100644 --- a/zh-cn/third-party-cases/Readme-CN.md +++ b/zh-cn/third-party-cases/Readme-CN.md @@ -17,6 +17,9 @@ - [如何实现抽屉式导航](navigation-drawer.md) - [如何实现内容下拉变化](content-changing-with-pulldown.md) - [如何实现圆形进度条](circle-progress-bar.md) +- [如何实现列表的二级联动](interact-lists.md) +- [时钟开发示例](time-styles-shift.md) +- [弹簧动画开发](how-to-develop-spring-animation.md) ### 网络管理 - [如何请求并加载网络图片](how-to-load-images-from-internet.md) diff --git a/zh-cn/third-party-cases/figures/clock.gif b/zh-cn/third-party-cases/figures/clock.gif new file mode 100644 index 0000000000000000000000000000000000000000..949d0d5e38de29ed25f90193fdf9ad2cbe669663 Binary files /dev/null and b/zh-cn/third-party-cases/figures/clock.gif differ diff --git a/zh-cn/third-party-cases/figures/responsivemotion.gif b/zh-cn/third-party-cases/figures/responsivemotion.gif new file mode 100644 index 0000000000000000000000000000000000000000..43b550646d44bf41d3016d7404f5b6bea8389e6b Binary files /dev/null and b/zh-cn/third-party-cases/figures/responsivemotion.gif differ diff --git a/zh-cn/third-party-cases/figures/secondarylinkage.gif b/zh-cn/third-party-cases/figures/secondarylinkage.gif new file mode 100644 index 0000000000000000000000000000000000000000..77c0c4641eae8c4d37656852e83d97efffdade4e Binary files /dev/null and b/zh-cn/third-party-cases/figures/secondarylinkage.gif differ diff --git a/zh-cn/third-party-cases/figures/springCurve.gif b/zh-cn/third-party-cases/figures/springCurve.gif new file mode 100644 index 0000000000000000000000000000000000000000..16e23ac58f11c15bdf628768a62a059315d540a9 Binary files /dev/null and b/zh-cn/third-party-cases/figures/springCurve.gif differ diff --git a/zh-cn/third-party-cases/figures/springanimation.gif b/zh-cn/third-party-cases/figures/springanimation.gif new file mode 100644 index 0000000000000000000000000000000000000000..388ac849813d1c83826af96e3f82773428094398 Binary files /dev/null and b/zh-cn/third-party-cases/figures/springanimation.gif differ diff --git a/zh-cn/third-party-cases/figures/springmotion.gif b/zh-cn/third-party-cases/figures/springmotion.gif new file mode 100644 index 0000000000000000000000000000000000000000..e5207d6673ff1fa3791a1055fcd578174c893c7e Binary files /dev/null and b/zh-cn/third-party-cases/figures/springmotion.gif differ diff --git a/zh-cn/third-party-cases/how-to-develop-spring-animation.md b/zh-cn/third-party-cases/how-to-develop-spring-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..0a22a5307720d70cd6dd7dbf67852cdee3c9cdb6 --- /dev/null +++ b/zh-cn/third-party-cases/how-to-develop-spring-animation.md @@ -0,0 +1,281 @@ +# 如何使用弹簧动画曲线 + +## 场景说明 +在动画开发场景中,经常用到弹性效果,尤其在拖拽某个对象时经常伴随弹性动效。OpenHarmony提供了三种弹簧动画曲线用来实现弹性效果,本例将为大家介绍这三种曲线的用法。 + +## 效果呈现 +本例最终效果如下: + +![springanimation](figures/springanimation.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 3.1 Beta2 +- SDK: Ohos_sdk_public 3.2.11.9(API Version 9 Release) + + +## 实现思路 +本例主要用到以下三种弹簧动画曲线: +- [curves.springCurve](../application-dev/reference/apis/js-apis-curve.md#curvesspringcurve9):通过设置弹簧的初始速度、质量、刚度和阻尼来控制弹簧动画的效果。对应本例中springCurve按钮触发的动画。 +- [curves.springMotion](../application-dev/reference/apis/js-apis-curve.md#curvesspringmotion9):通过设置弹簧震动时间和阻尼来控制弹簧动画的效果。对应本例中springMotion按钮触发的动画。 +- [curves.responsiveSpringMotion](../application-dev/reference/apis/js-apis-curve.md#curvesresponsivespringmotion9):构造弹性跟手动画曲线对象,是springMotion的一种特例,仅默认参数不同,可与springMotion混合使用。用来实现拖拽动画。 + +## 开发步骤 +1. 搭建UI框架。 +样例中有两个按钮,一个图片。内容整体纵向分布,两个按钮横向分布。纵向布局可以采用Column组件,横向布局可以采用Row组件。代码如下: + ```ts + @Entry + @Component + struct ImageComponent { + build() { + Column() { + Row() { + Button('springCurve') + .margin({right:10}) + .fontSize(20) + .backgroundColor('#18183C') + Button('springMotion') + .fontSize(20) + .backgroundColor('#18183C') + } + .margin({top:30}) + + Image($r("app.media.contact2")) + .width(100) + .height(100) + }.width("100%").height("100%").backgroundColor('#A4AE77') + } + } + ``` +2. 为springCurve按钮添加curves.springCurve的曲线动画。 + ```ts + ... + // 定义状态变量translateY,用来控制笑脸图像的位移 + @State translateY: number = 0 + ... + Button('springCurve') + .margin({right:10}) + .fontSize(20) + .backgroundColor('#18183C') + // 绑定点击事件 + .onClick(() => { + // 在点击事件中添加显示动画 + animateTo({ + duration: 2000, + // 设定curves.springCurve为动画曲线 + curve: curves.springCurve(100, 10, 80, 10) + }, + () => { + // 改变translateY的值,使笑脸图像发生位移 + this.translateY = -20 + }) + this.translateY = 0 + }) + ... + Image($r("app.media.contact2")) + .width(100) + .height(100) + // 为笑脸图像添加位移属性,以translateY为参数 + .translate({ y: this.translateY }) + ... + ``` + 效果如下: + + ![springCurve](figures/springCurve.gif) +3. 为springMotion按钮添加curves.springMotion曲线动画。 +这里通过position属性控制springMotion按钮的移动,当然开发者也可以继续选择使用translate属性。 + ```ts + ... + // 定义状态变量translateY,用来控制笑脸图像的位置变化 + @State imgPos: { + x: number, + y: number + } = { x: 125, y: 400 } + ... + Button('springMotion') + .fontSize(20) + .backgroundColor('#18183C') + // 绑定点击事件 + .onClick(() => { + // 在点击事件中添加显示动画 + animateTo({ + duration: 15, + //设定curves.springMotion为动画曲线 + curve: curves.springMotion(0.5, 0.5), + onFinish: () => { + animateTo({ duration: 500, + curve: curves.springMotion(0.5, 0.5), }, () => { + // 动画结束时笑脸图像位置还原 + this.imgPos = { x: 125, y: 400 } + }) + } + }, () => { + // 改变笑脸图像位置,y轴位置由400,变为150 + this.imgPos = { x: 125, y: 150 } + }) + }) + ... + Image($r("app.media.contact2")) + .width(100) + .height(100) + .translate({ y: this.translateY }) + // 为笑脸图像添加位置属性,以imgPos为参数 + .position(this.imgPos) + ... + ``` + 效果如下: + + ![springmotion](figures/springmotion.gif) +4. 使用curves.responsiveSpringMotion为笑脸图像添加拖拽动画。 + ```ts + ... + Image($r("app.media.contact2")) + .width(100) + .height(100) + .translate({ y: this.translateY }) + .position(this.imgPos) + // 绑定触摸事件 + .onTouch((event: TouchEvent) => { + // 当触摸放开时,笑脸图像位置还原 + if (event.type == TouchType.Up) { + animateTo({ + duration: 50, + delay: 0, + curve: curves.springMotion(), + onFinish: () => { + } + }, () => { + this.imgPos = { x: 125, y: 400 } + }) + } else { + // 触摸过程中触发跟手动画 + animateTo({ + duration: 50, + delay: 0, + //设定跟手动画曲线 + curve: curves.responsiveSpringMotion(), + onFinish: () => { + } + }, () => { + // 根据触点位置改变笑脸图像位置,从而实现跟手动画 + this.imgPos = { + x: event.touches[0].screenX - 100 / 2, + y: event.touches[0].screenY - 100 / 2 + } + }) + } + }) + ... + ``` + 效果如下: + + ![responsivemotion](figures/responsivemotion.gif) + +## 完整代码 +本例完整代码如下: +```ts +import curves from '@ohos.curves'; + +@Entry +@Component +struct ImageComponent { + // 定义状态变量translateY,用来控制笑脸图像的位移 + @State translateY: number = 0 + // 定义状态变量translateY,用来控制笑脸图像的位置变化 + @State imgPos: { + x: number, + y: number + } = { x: 125, y: 400 } + + build() { + Column() { + Row() { + Button('springCurve') + .margin({right:10}) + .fontSize(20) + .backgroundColor('#18183C') + // 绑定点击事件 + .onClick(() => { + // 在点击事件中添加显示动画 + animateTo({ + duration: 2000, + // 设定curves.springCurve为动画曲线 + curve: curves.springCurve(100, 10, 80, 10) + }, + () => { + // 改变translateY的值,使笑脸图像发生位移 + this.translateY = -20 + }) + this.translateY = 0 + }) + Button('springMotion') + .fontSize(20) + .backgroundColor('#18183C') + // 绑定点击事件 + .onClick(() => { + // 在点击事件中添加显示动画 + animateTo({ + duration: 15, + //设定curves.springMotion为动画曲线 + curve: curves.springMotion(0.5, 0.5), + onFinish: () => { + animateTo({ duration: 500, + curve: curves.springMotion(0.5, 0.5), }, () => { + // 动画结束时笑脸图像位置还原 + this.imgPos = { x: 125, y: 400 } + }) + } + }, () => { + // 改变笑脸图像位置,y轴位置由400,变为150 + this.imgPos = { x: 125, y: 150 } + }) + }) + } + .margin({top:30}) + + Image($r("app.media.contact2")) + .width(100) + .height(100) + // 为笑脸图像添加位移属性,以translateY为参数 + .translate({ y: this.translateY }) + // 为笑脸图像添加位置属性,以imgPos为参数 + .position(this.imgPos) + // 绑定触摸事件 + .onTouch((event: TouchEvent) => { + // 当触摸放开时,笑脸图像位置还原 + if (event.type == TouchType.Up) { + animateTo({ + duration: 50, + delay: 0, + curve: curves.springMotion(), + onFinish: () => { + } + }, () => { + this.imgPos = { x: 125, y: 400 } + }) + } else { + // 触摸过程中触发跟手动画,同样通过animateTo实现动画效果 + animateTo({ + duration: 50, + delay: 0, + //设定跟手动画曲线 + curve: curves.responsiveSpringMotion(), + onFinish: () => { + } + }, () => { + // 根据触点位置改变笑脸图像位置,从而实现跟手动画 + this.imgPos = { + x: event.touches[0].screenX - 100 / 2, + y: event.touches[0].screenY - 100 / 2 + } + }) + } + }) + }.width("100%").height("100%").backgroundColor('#A4AE77') + } +} +``` +## 参考 +- [显示动画](../application-dev/reference/arkui-ts/ts-explicit-animation.md) +- [插值计算](../application-dev/reference/apis/js-apis-curve.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/interact-lists.md b/zh-cn/third-party-cases/interact-lists.md new file mode 100644 index 0000000000000000000000000000000000000000..63ce50c3bdbb640794e8ffb489f4b8b1eca84d78 --- /dev/null +++ b/zh-cn/third-party-cases/interact-lists.md @@ -0,0 +1,236 @@ +# 二级联动 +## 场景介绍 +列表的二级联动(Cascading List)是指根据一个列表(一级列表)的选择结果,来更新另一个列表(二级列表)的选项。这种联动可以使用户根据实际需求,快速定位到想要的选项,提高交互体验。例如,短视频中拍摄风格的选择、照片编辑时的场景的选择,本文即为大家介绍如何开发二级联动。 +## 效果呈现 +本例最终效果如下: + +![](figures/secondarylinkage.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: +- IDE: DevEco Studio 3.1 Beta2 +- SDK: Ohos_sdk_public 3.2.11.9 (API Version 9 Release) +## 实现思路 +- 数字标题(titles)以及下方的数字列表(contents)分组展示:通过两个List组件分别承载数字标题和数字项。 +- 滚动数字列表,上方数字标题也随之变动:通过List组件的onScrollIndex事件获取到当前滚动数字的索引,根据该索引计算出对应标题数字的索引,然后通过Scroller的scrollToIndex方法跳转到对应的数字标题,且通过Line组件为选中的标题添加下划线。 +- 点击数字标题,下方的数字列表也随之变化:首先获取到点击数字标题的索引,通过该索引计算出下方对应数字的起始项索引,然后通过scroller的scrollToIndex方法跳转到对应索引的数字项。 + +## 开发步骤 +根据实现思路,具体实现步骤如下: +1. 首先构建列表数据,在records中记录数字列表中各个数字的首项索引值,具体代码块如下: + ```ts + ... + @State typeIndex: number = 0 + private tmp: number = 0 + private titles: Array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + private contents: Array = ["1", "1", "1", "1", "1", "1", "1", "1", "1", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "3" + , "3", "3", "3", "3", "4", "4", "4", "5", "5", "5", "5", "5", "6", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", + "8", "8", "8", "8", "8", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9"] + private records: Array = [0, 9, 21, 26, 29, 34, 35, 47, 52, 63] + private classifyScroller: Scroller = new Scroller(); + private scroller: Scroller = new Scroller(); + ... + ``` + 数字标题列表:具体代码块如下: + ```ts + ... + build() { + Column({ space: 0 }) { + List ({ space: 50, scroller: this.classifyScroller, initialIndex: 0 }) { + ForEach(this.titles, (item, index) => { + ListItem() { + Column() { + Text(item) + .fontSize(14) + ... + } + } + } + ... + } + .listDirection(Axis.Horizontal) + .height(50) + } + } + ``` + 数字列表,具体代码块如下: + ```ts + List({ space: 20, scroller: this.scroller }) { + ForEach(this.contents, (item, index) => { + ListItem() { + Column({ space: 5 }) { + Image($r("app.media.app_icon")) + .width(40) + .height(40) + Text(item) + .fontSize(12) + } + ... + } + } + } + .listDirection(Axis.Horizontal) //列表排列方向水平 + .edgeEffect(EdgeEffect.None) //不支持滑动效果 + ``` +2. 数字标题的索引值判断,根据当前滚动数字的首项索引值计算出数字标题的索引,具体代码块如下: + ```ts + ... + findClassIndex(ind: number) { // 当前界面最左边图的索引值ind + let ans = 0 + // 定义一个i 并进行遍历 this.records.length = 10 + for (let i = 0; i < this.records.length; i++) { + // 判断ind在this.records中那两个临近索引值之间 + if (ind >= this.records[i] && ind < this.records[i + 1]) { + ans = i + break + } + } + return ans + } + findItemIndex(ind: number) { + // 将ind重新赋值给类型标题列表的索引值 + return this.records[ind] + } + ... + ``` + 通过Line组件构成标题下滑线,具体代码块如下: + ```ts + ... + if (this.typeIndex == index) { + Line() + //根据长短判断下划线 + .width(item.length === 2 ? 25 : item.length === 3 ? 35 : 50) + .height(3) + .strokeWidth(20) + .strokeLineCap(LineCapStyle.Round) + .backgroundColor('#ffcf9861') + } + ... + ``` +3. 点击数字标题,数字列表随之滑动:首先获取到点击数字标题的索引,通过该索引计算出下方对应数字的起始项索引,然后通过scroller的scrollToIndex方法跳转到对应索引的数字项,具体代码块如下: + ```ts + ... + .onClick(() => { + this.typeIndex = index + this.classifyScroller.scrollToIndex(index) + let itemIndex = this.findItemIndex(index) + console.log("移动元素:" + itemIndex) + this.scroller.scrollToIndex(itemIndex) + }) + ... + ``` +4. 数字列表的滑动或点击导致数字标题的变动:通过List组件中onScrollIndex事件获取的到屏幕中最左边数字的索引值start,然后通过该索引值计算出对应的数字标题的索引currentClassIndex,然后通过scrollToIndex控制数字标题跳转到对应索引处,具体代码块如下: + ```ts + ... + .onScrollIndex((start) => { + let currentClassIndex = this.findClassIndex(start) + console.log("找到的类索引为: " + currentClassIndex) + if (currentClassIndex != this.tmp) { + this.tmp = currentClassIndex + console.log("类别移动到索引: " + currentClassIndex) + this.typeIndex = currentClassIndex + this.classifyScroller.scrollToIndex(currentClassIndex) + } + }) + ... + ``` +## 完整代码 +完整示例代码如下: +```ts +@Entry +@Component +struct TwoLevelLink { + @State typeIndex: number = 0 + private tmp: number = 0 + private titles: Array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + private contents: Array = ["1", "1", "1", "1", "1", "1", "1", "1", "1", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "3" + , "3", "3", "3", "3", "4", "4", "4", "5", "5", "5", "5", "5", "6", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", + "8", "8", "8", "8", "8", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9"] + private colors: Array = ["#18183C", "#E8A027", "#D4C3B3", "#A4AE77", "#A55D51", "#1F3B54", "#002EA6", "#FFE78F", "#FF770F"] + private records: Array = [0, 9, 21, 26, 29, 34, 35, 47, 52, 63] + private classifyScroller: Scroller = new Scroller(); + private scroller: Scroller = new Scroller(); + // 根据数字列表索引计算对应数字标题的索引 + findClassIndex(ind: number) { + let ans = 0 + for (let i = 0; i < this.records.length; i++) { + if (ind >= this.records[i] && ind < this.records[i + 1]) { + ans = i + break + } + } + return ans + } + // 根据数字标题索引计算对应数字列表的索引 + findItemIndex(ind: number) { + return this.records[ind] + } + build() { + Column({ space: 0 }) { + List ({ space: 50, scroller: this.classifyScroller, initialIndex: 0 }) { + ForEach(this.titles, (item, index) => { + ListItem() { + Column() { + Text(item) + .fontSize(24) + if (this.typeIndex == index) { + Line() + .width(item.length === 2 ? 25 : item.length === 3 ? 35 : 50) + .height(3) + .strokeWidth(20) + .strokeLineCap(LineCapStyle.Round) + .backgroundColor('#ffcf9861') + } + } + .onClick(() => { + this.typeIndex = index + this.classifyScroller.scrollToIndex(index) + let itemIndex = this.findItemIndex(index) + console.log("移动元素:" + itemIndex) + this.scroller.scrollToIndex(itemIndex) + }) + } + }) + } + .listDirection(Axis.Horizontal) + .height(50) + List({ space: 20, scroller: this.scroller }) { + ForEach(this.contents, (item, index) => { + ListItem() { + Column({ space: 5 }) { + Text(item) + .fontSize(30) + .fontColor(Color.White) + } + .width(60) + .height(60) + .backgroundColor(this.colors[item-1]) + .justifyContent(FlexAlign.Center) + .onClick(() => { + this.scroller.scrollToIndex(index) + }) + } + }) + } + .listDirection(Axis.Horizontal) //列表排列方向水平 + .edgeEffect(EdgeEffect.None) //不支持滑动效果 + .onScrollIndex((start) => { + let currentClassIndex = this.findClassIndex(start) + console.log("找到的类索引为: " + currentClassIndex) + if (currentClassIndex != this.tmp) { + this.tmp = currentClassIndex + console.log("类别移动到索引: " + currentClassIndex) + this.typeIndex = currentClassIndex + this.classifyScroller.scrollToIndex(currentClassIndex) + } + }) + }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 }) + } +} +``` +## 参考 +[List](../application-dev/reference/arkui-ts/ts-container-list.md) + +[Line](../application-dev/reference/arkui-ts/ts-drawing-components-line.md) + +[Scroll](../application-dev/reference/arkui-ts/ts-container-scroll.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/time-styles-shift.md b/zh-cn/third-party-cases/time-styles-shift.md new file mode 100644 index 0000000000000000000000000000000000000000..cf0dc81099380c195f328f25b31823ec529f3350 --- /dev/null +++ b/zh-cn/third-party-cases/time-styles-shift.md @@ -0,0 +1,354 @@ +# 时钟开发 +## 场景介绍 +常见的时钟呈现方式有两种,一种是表盘方式,一种是数字方式。用户可根据个人喜好在两种形式间进行切换。本例即为大家讲解如何开发上述两种钟表样式,以供参考。 +## 效果呈现 +本例最终效果如下: + +![](figures/clock.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: +- IDE: DevEco Studio 3.1 Beta2 +- SDK: Ohos_sdk_public 3.2.11.9 (API Version 9 Release) +## 实现思路 +- 表盘方式的展示:通过Canvas组件提供画布;在画布上,通过CanvasRenderingContext2D对象使用RenderingContext在Canvas组件上进行绘制,绘制表盘上的数字、时针、分针、秒针。表盘上数字的分布使用fillText绘制填充类文本并确定其在画布上位置;表盘上时针的运动通过theta的角度决定时针的移动;分针和秒针同上。 +- 数字时间方式的展示:使用TextClock组件通过文本将系统时间显示在设备上。 +## 开发步骤 +根据上述思路,具体实现步骤如下: +1. 表盘方式:通过CanvasRenderingContext2D对象使用RenderingContext在Canvas组件上进行绘制,绘制表盘上的数字、时针、分针、秒针。 + 首先,创建画布,具体代码如下: + ```ts + // clock ets + clear() { // clear canvas function + this.ctx.clearRect(0, 0, 360, 500); + } + drawScene() { // main drawScene function 绘制场景 + this.clear(); // clear canvas + ... + build() { + Column({ space: 5 }) { + Canvas(this.ctx) + .width(360) + .height(500) + .border({ width: 1, color: '#ffff00'}) + .onReady(() => { + setInterval(() => { + this.drawScene() + }, 1000) + }) + ... + } + } + ``` + 声明相关变量,具体代码如下: + ```ts + // clock ets + let date = new Date(); + let hours = date.getHours(); + let minutes = date.getMinutes(); + let seconds = date.getSeconds(); + hours = hours > 12 ? hours - 12 : hours; + let hour = hours + minutes / 60; + let minute = minutes + seconds / 60; + ``` + 使用fillText方法绘制表盘数字并确定其位置 + ```ts + // clock ets + ... + // draw numbers + this.ctx.font = '36px Arial'; //文本尺寸 + this.ctx.fillStyle = '#000'; //指定绘制的填充色 + this.ctx.textAlign = 'center'; // 文本对齐 + this.ctx.textBaseline = 'middle'; //文本基线 + for (let n = 0; n < 12; n++) { + let theta = (n - 2) * (Math.PI * 2) / 12; + let x = clockRadius * 0.7 * Math.cos(theta); + let y = clockRadius * 0.7 * Math.sin(theta); + this.ctx.fillText(`${n + 1}`, x, y); // 表盘数字所在的位置 + ... + ``` + 时针的移动路径,具体代码如下: + ```ts + // clock ets + ... + // draw hour + this.ctx.save(); //将当前状态放入栈中,保存canvas的全部状态,通常在需要保存绘制状态时调用 + let theta = (hour - 3) * 2 * Math.PI / 12; + this.ctx.rotate(theta); //顺时针旋转 + this.ctx.beginPath(); //创建一个新的绘制路径 + this.ctx.moveTo(-15, -5); //绘制时针组件 起始点 + this.ctx.lineTo(-15, 5); + this.ctx.lineTo(clockRadius * 0.3, 1); + this.ctx.lineTo(clockRadius * 0.3, -1); //绘制时针组件 终点 + this.ctx.fillStyle = 'green'; + this.ctx.fill(); + this.ctx.restore(); //对保存的绘图上下文进行恢复 + ... + ``` + 分针的移动路径,具体代码如下: + ```ts + // clock ets + ... + // draw minute + this.ctx.save(); + theta = (minute - 15) * 2 * Math.PI / 60; + this.ctx.rotate(theta); //顺时针旋转 + this.ctx.beginPath(); //创建一个新的绘制路径 + this.ctx.moveTo(-15, -4);//绘制分针组件 起始点 + this.ctx.lineTo(-15, 4); + this.ctx.lineTo(clockRadius * 0.45, 1); + this.ctx.lineTo(clockRadius * 0.45, -1);//绘制分针组件 终点 + this.ctx.fillStyle = 'red'; + this.ctx.fill(); + this.ctx.restore(); //对保存的绘图上下文进行恢复 + ... + ``` + 秒针的移动路径,具体代码如下: + ```ts + // clock ets + ... + // draw second + this.ctx.save(); + theta = (seconds - 15) * 2 * Math.PI / 60; + this.ctx.rotate(theta); //顺时针旋转 + this.ctx.beginPath(); //创建一个新的绘制路径 + this.ctx.moveTo(-15, -3);//绘制秒针组件 起始点 + this.ctx.lineTo(-15, 3); + this.ctx.lineTo(clockRadius * 0.6, 1); + this.ctx.lineTo(clockRadius * 0.6, -1);//绘制秒针组件 终点 + this.ctx.fillStyle = 'black'; + this.ctx.fill(); + this.ctx.restore(); //对保存的绘图上下文进行恢复 + ... + ``` +2. 时钟方式的转换:通过Button组件中的onClick事件进行切换页面。 + 从表盘方式往数字方式转换,具体代码如下: + ```ts + // clock.ets + ... + Button(){ + Text("切换") + .fontSize(30) + .fontWeight(FontWeight.Regular) + } + .type(ButtonType.Capsule) + .margin({top:20 + }) + .backgroundColor("red") + .width('40%') + .height('5%') + .onClick(()=>{ + router.pushUrl({url:'pages/Index1'}) + }) + ... + ``` + 从数字时间方式往表盘方式转换,具体代码如下: + ```ts + // TextClock.ets + ... + Button() { + Text("切换") + .fontSize(30) + .fontWeight(FontWeight.Regular) + } + .type(ButtonType.Capsule) + .margin({ top: 20 + }) + .backgroundColor("red") + .width('40%') + .height('5%') + .onClick(() => { + router.back() + }) + ... + ``` +3. 数字时间方式:使用TextClock组件通过文本将当前系统时间显示在设备上。 + 具体代码如下: + ```ts + // TextClock.ets + import router from '@ohos.router' + @Entry + @Component + struct Second { + @State accumulateTime: number = 0 + // 导入对象 + controller: TextClockController = new TextClockController() + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + TextClock({ timeZoneOffset: -8, controller: this.controller }) + .format('hms') //数字时间格式 + .onDateChange((value: number) => { + this.accumulateTime = value + }) + .margin(20) + .fontSize(30) + ... + } + } + } + ``` +## 完整代码 +完整示例代码如下: +表盘时钟代码页 +```ts +// clock.ets +import router from '@ohos.router'; +const clockRadius = 180; + +@Entry +@Component +struct Test10 { + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + + // 绘制函数 + clear() { + this.ctx.clearRect(0, 0, 360, 500); + } + drawScene() { // 绘制场景 + this.clear(); // 清空画布 + // 获取当前时间 + let date = new Date(); + let hours = date.getHours(); + let minutes = date.getMinutes(); + let seconds = date.getSeconds(); + hours = hours > 12 ? hours - 12 : hours; + let hour = hours + minutes / 60; + let minute = minutes + seconds / 60; + this.ctx.save(); + this.ctx.translate(360 / 2, 500 / 2); + this.ctx.beginPath(); //创建一个新的绘制路径 + + // 绘制表盘数字 + this.ctx.font = '45px Arial'; //文本尺寸 + this.ctx.fillStyle = '#000'; //指定绘制的填充色 + this.ctx.textAlign = 'center'; // 文本对齐 + this.ctx.textBaseline = 'middle'; //文本基线 + for (let n = 0; n < 12; n++) { + let theta = (n - 2) * (Math.PI * 2) / 12; + let x = clockRadius * 0.7 * Math.cos(theta); + let y = clockRadius * 0.7 * Math.sin(theta); + this.ctx.fillText(`${n + 1}`, x, y); // 表盘数字所在的位置 + } + + // 绘制时针 + this.ctx.save(); //将当前状态放入栈中,保存canvas的全部状态,通常在需要保存绘制状态时调用 + let theta = (hour - 3) * 2 * Math.PI / 12; + this.ctx.rotate(theta); //顺时针旋转 + this.ctx.beginPath(); //创建一个新的绘制路径 + this.ctx.moveTo(-15, -5); //绘制时针组件 起始点 + this.ctx.lineTo(-15, 5); + this.ctx.lineTo(clockRadius * 0.3, 1); + this.ctx.lineTo(clockRadius * 0.3, -1); + this.ctx.fillStyle = 'green'; + this.ctx.fill(); + this.ctx.restore(); //对保存的绘图上下文进行恢复 + + // 绘制分针 + this.ctx.save(); + theta = (minute - 15) * 2 * Math.PI / 60; + this.ctx.rotate(theta); //顺时针旋转 + this.ctx.beginPath(); //创建一个新的绘制路径 + this.ctx.moveTo(-15, -4); //绘制分针组件 起始点 + this.ctx.lineTo(-15, 4); + this.ctx.lineTo(clockRadius * 0.45, 1); + this.ctx.lineTo(clockRadius * 0.45, -1); + this.ctx.fillStyle = 'red'; + this.ctx.fill(); + this.ctx.restore(); //对保存的绘图上下文进行恢复 + + // 绘制秒针 + this.ctx.save(); + theta = (seconds - 15) * 2 * Math.PI / 60; + this.ctx.rotate(theta); //顺时针旋转 + this.ctx.beginPath(); //创建一个新的绘制路径 + this.ctx.moveTo(-15, -3); //绘制秒针组件 起始点 + this.ctx.lineTo(-15, 3); + this.ctx.lineTo(clockRadius * 0.6, 1); + this.ctx.lineTo(clockRadius * 0.6, -1); + this.ctx.fillStyle = 'black'; + this.ctx.fill(); + this.ctx.restore(); //对保存的绘图上下文进行恢复 + + this.ctx.restore(); //对保存的绘图上下文进行恢复 + } + + build() { + Column({ space: 5 }) { + Canvas(this.ctx) + .width(360) + .height(500) + .onReady(() => { + setInterval(() => { + this.drawScene() + }, 1000) + }) + Button(){ + Text("切换") + .fontSize(30) + .fontWeight(FontWeight.Regular) + } + .type(ButtonType.Capsule) + .margin({top:20 + }) + .backgroundColor('#E8A027') + .width('40%') + .height('5%') + .onClick(()=>{ + router.pushUrl({url:'pages/TextClock'}) + }) + }.width('100%') + .height('100%') + .backgroundColor('#A4AE75') + } +} +``` +数字时间代码页: +```ts +//TextClock.ets +import router from '@ohos.router' +@Entry +@Component +struct Second { + @State accumulateTime: number = 0 + // 导入对象 + controller: TextClockController = new TextClockController() + + build() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + + TextClock({ timeZoneOffset: -8, controller: this.controller }) //timeZoneOffset 时区偏移ian + .format('hms') + .onDateChange((value: number) => { + this.accumulateTime = value + }) + .margin(20) + .fontSize(30) + Button() { + Text("切换") + .fontSize(30) + .fontWeight(FontWeight.Regular) + } + .type(ButtonType.Capsule) + .margin({ top: 20 + }) + .backgroundColor('#E8A027') + .width('40%') + .height('5%') + .onClick(() => { + router.back() + }) + } + .width('100%') + .height('100%') + .backgroundColor('#D4C3B3') + } +} +``` +## 参考 +[Canvas](../application-dev/reference/arkui-ts/ts-components-canvas-canvas.md) + +[CanvasRenderingContext2D对象](../application-dev/reference/arkui-ts/ts-canvasrenderingcontext2d.md) + +[TextClock](../application-dev/reference/arkui-ts/ts-basic-components-textclock.md) \ No newline at end of file