134.鸿蒙基础02

嚴華  论坛元老 | 2024-11-18 08:28:04 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1047|帖子 1047|积分 3141

沉浸式导航+键盘避让

:::success
官方文档(有权限者可观看): https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-develop-apply-immersive-effects-0000001820435461
:::


  • 沉浸式导航
准备一张占满整个屏幕的图片,以我的自拍为例,假如你是在预览器里面打开

这么看没有题目,假如你是在模拟器大概真机打开,你会发现上下两部分被空出来

:::info
空出来的这两部分叫做安全区,所谓沉浸式指的得就是关闭内置安全区,自己添补安全区的内容
:::
:::success
怎么实现呢?
两种方案

  • 使用windowStage的设置全屏的方式
  • 使用组件的安全区域扩展的方式
    :::
使用windowStage来设置

:::success
window非前端window,鸿蒙中属于窗口管理对象,
:::
:::success
在ability中通过getMainWindow可以获取主窗体,然后通过得到的window对象设置全屏即可实现
:::
  1. windowStage.getMainWindow().then(window => {
  2.       window.setWindowLayoutFullScreen(true)
  3. })
复制代码

:::success
通过这种方式最简朴,但是相当于给全部的页面都设置了沉浸式,假如某些页面不需要设置沉浸式,还需要在页面中通过获取window来关闭
:::



  • 页面中关闭沉浸式
  1. aboutToAppear(): void {
  2.     window.getLastWindow(getContext())
  3.       .then(win => {
  4.         win.setWindowLayoutFullScreen(false)
  5.       })
  6.   }
复制代码

:::success
还有个题目,假如想要获取安全区域的高度,然后在安全区域做些间隔的拉开。
:::
:::success
获取安全区域的高度
getWindowAvoidArea(传入上大概下)
:::
  1.     const win = await window.getLastWindow(getContext())
  2.     // 关闭沉浸式
  3.     win.setWindowLayoutFullScreen(false)
  4.     // 获取上方安全区高度
  5.     this.topSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
  6.       .topRect.height)
  7.     // 获取下方安全区高度
  8.     this.bottomSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
  9.       .bottomRect.height)
  10.     AlertDialog.show({
  11.       message: `
  12.       上安全区:${this.topSafeHeight}
  13.       下安全区:${this.bottomSafeHeight}`
  14.     })
复制代码

:::success
由于获取的安全区域的大小是px,以是需要用到vp的话 需要使用pxtovp的方法来实现
:::


  • 开启沉浸式页面自界说安全区颜色

  1. import { window } from '@kit.ArkUI'
  2. @Entry
  3. @Component
  4. struct SafeAreaCase {
  5.   @State
  6.   topSafeHeight: number = 0
  7.   @State
  8.   bottomSafeHeight: number = 0
  9.   async aboutToAppear() {
  10.     const win = await window.getLastWindow(getContext())
  11.     // 防止全局没开启,指定页面开启沉浸式
  12.     win.setWindowLayoutFullScreen(true)
  13.     // 获取上方安全区高度
  14.     this.topSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
  15.       .topRect.height)
  16.     // 获取下方安全区高度
  17.     this.bottomSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
  18.       .bottomRect.height)
  19.     AlertDialog.show({
  20.       message: `
  21.       上安全区:${this.topSafeHeight}
  22.       下安全区:${this.bottomSafeHeight}`
  23.     })
  24.   }
  25.   build() {
  26.     Column() {
  27.       Image($r('app.media.b'))
  28.     }
  29.     .width('100%')
  30.     .height('100%')
  31.     .padding({
  32.       top: this.topSafeHeight,
  33.       bottom: this.bottomSafeHeight
  34.     })
  35.     .backgroundColor(Color.Green)
  36.     .backgroundImageSize({ width: '100%', height: '100%' })
  37.   }
  38. }
复制代码
安全区域expandSafeArea

:::success
文档地点(有权限才可看): https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-expand-safe-area-0000001820880849#ZH-CN_TOPIC_0000001820880849__expandsafearea
:::
:::success
相对于上述通过window设置全部页面进行全局的设置,expandSafeArea是个按需的方式,哪个页面需要使用
沉浸式,直接自己设置即可。


  • 作用控制组件扩展其安全区域。
    :::
  1. Image($r("app.media.handsome"))
  2.         .width('100%')
  3.         .height('50%')
  4.         .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
复制代码
键盘避让模式

:::success
当我们存在输入框的页面,假如点击输入框,此时就会弹出键盘,此时键盘的弹出会出题目,如下图
:::

  1. @Entry
  2. @Component
  3. struct KeyCase {
  4.   build() {
  5.     Column() {
  6.       Row() {
  7.         Text("顶部内容")
  8.       }
  9.       .justifyContent(FlexAlign.Center)
  10.       .height(50)
  11.       .width('100%')
  12.       Column() {
  13.         Text("中间内容")
  14.       }
  15.       .justifyContent(FlexAlign.Center)
  16.       .backgroundColor(Color.Orange)
  17.       .width('100%')
  18.       .layoutWeight(1)
  19.       Row() {
  20.         TextInput({ placeholder: '请输入内容' })
  21.           .width('100%')
  22.       }
  23.       .padding({
  24.         left: 10,
  25.         right: 10
  26.       })
  27.       .justifyContent(FlexAlign.Center)
  28.       .height(50)
  29.       .width('100%')
  30.     }
  31.     .width('100%')
  32.     .height('100%')
  33.   }
  34. }
复制代码

:::success
我们可以设置键盘的避让模式,让窗口被键盘压缩,默认情况下,窗口和键盘的情况是如许的

设置为压缩就酿成

:::


  • 设置方式 - ability中设置
  1. import {  KeyboardAvoidMode } from '@kit.ArkUI';
  2. windowStage.getMainWindowSync().getUIContext()
  3.         .setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
复制代码


路由控制

:::success
路由控制有多种方式来实现
官方阐明-


  • router方式-更适用用于模块间与模块内页面切换,通过每个页面的url实现模块间解耦
  • Naviagtion-模块内页面跳转时,为了实现更好的转场动效场景不建议使用router该模块,保举使用Navigation
    :::
    :::success
    项目中现实还是适用router更较为简朴和合理
    Navigation的方式更适合简朴页面的方式
    :::
Navigtion的使用

:::success
Navigation组件是路由导航的根视图容器,一样寻常作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页表现导航内容(Navigation的子组件)或非首页表现(NavDestination的子组件),首页和非首页通过路由进行切换。
:::
:::success
使用Navigation跳转的组件不需要再使用Entry来修饰,普通组件即可
:::


  • 用法
    :::success
    Navigation是一个导航组件,API9和API11的使用官方的保举方式各不相同
    :::
    API9的用法- Navigation-NavRouter-(其他组件+NavDestination的用法)
  1. @Entry
  2. @Component
  3. struct NavigationCase {
  4.   build() {
  5.     // API9
  6.     Navigation() {
  7.       NavRouter() {
  8.         // 只能放一个组件
  9.         Column() {
  10.           Text('A页面的')
  11.           Button('去B页面')
  12.           Button('去B2页面')
  13.         }
  14.         // 第二个会替换第一个,并且点击会进行跳转
  15.         Image($r('app.media.b'))
  16.           .height(100)
  17.         //要跳转的页面
  18.         NavDestination() {
  19.           NavRouter() {
  20.             Column() {
  21.               Text('B页面的')
  22.               Button('去C页面')
  23.             }
  24.             NavDestination() {
  25.               Text('C页面的')
  26.             }
  27.             .title('页面的返回标题')
  28.           }
  29.         }
  30.       }
  31.     }
  32.   }
  33. }
复制代码
A:

B:

C:

:::success
使用Navigation在不同装备中会得到不同的视觉体验
:::

api11- Navigation-NavPathStack
:::info
用法调整:使用NavPathStack+.navDestination()控制
1.主页内容仍旧写在Navigation中,跳转不再需要NavRouter组件,而是new NavPathStack()后跳转
2.跳转的页面放置.navDestination()中,传入一个自界说构建函数,在函数中条件渲染
总结:new NavPathStack()后的实例可以用于跳转,无论如何都会打开一个页面,渲染的内容仍旧是.navDestination()中条件渲染的内容,假如没有满足的条件,就是一个空缺页
:::
滚动缩小标题
!


隐蔽标题栏

自界说返回图标+带参数跳转
!


不满足条件渲染时表现空缺页面

  1. @Entry
  2. @Component
  3. struct NavigationCase02 {
  4.   // API11
  5.   @Builder
  6.   navDesBuilder(name: string) {
  7.     // 这个builder用于条件渲染展示的页面
  8.     if (name === 'one'){
  9.       // 必须用NavDestination才能显示
  10.       NavDestination() {
  11.         Text('one')
  12.         Button('去TWO')
  13.           .onClick(()=>{
  14.             this.navPathStack.pushPath({
  15.               name:'two'
  16.             })
  17.           })
  18.       }.title('123')
  19.       .hideTitleBar(true)
  20.       // .mode(NavDestinationMode.DIALOG)
  21.     }
  22.     else if (name === 'two') {
  23.       // 可以用组件
  24.       NavigationChild()
  25.     }
  26.   }
  27.   // 1.NavPathStack + navDestination(NavDestination)
  28.   @Provide
  29.   navPathStack: NavPathStack = new NavPathStack()
  30.   build() {
  31.     Navigation(this.navPathStack) {
  32.       // FREE类型滚动时会自动收起
  33.       Scroll(){
  34.         Column({space:20}) {
  35.           Button('下一页')
  36.             .onClick(() => {
  37.               this.navPathStack.pushPath({
  38.                 name: 'one'
  39.               })
  40.             })
  41.           Image($r('app.media.b'))
  42.           Image($r('app.media.b'))
  43.           Image($r('app.media.b'))
  44.         }
  45.       }
  46.     }
  47.     .navDestination(this.navDesBuilder)
  48.     .title('西北吴彦祖')
  49.     .titleMode(NavigationTitleMode.Free)
  50.   }
  51. }
  52. @Component
  53. struct NavigationChild {
  54.   @Consume
  55.   navPathStack:NavPathStack
  56.   build() {
  57.     NavDestination() {
  58.       Text('two')
  59.       Button('去THREE')
  60.         .onClick(()=>{
  61.           // 只要跳了,就会有个页面,只不过条件渲染没匹配上,不知道渲染什么
  62.           this.navPathStack.pushPath({
  63.             name:'three'
  64.           })
  65.         })
  66.     }
  67.     .backButtonIcon($r('app.media.a'))
  68.   }
  69. }
  70. @Component
  71. struct NavigationChild2 {
  72.   build() {
  73.     NavDestination() {
  74.       Text('three')
  75.     }
  76.   }
  77. }
复制代码
:::success
Navigation的这种跳转方式自带适配方案和转场动画,有特色但不容易定制,根据设计稿选择是否需要使用
:::
router的使用

:::success
router的使用都是基于Entry修饰的组件
都是基于resources/base/profile/main-page.json中的路由配置来跳转的
:::
:::success
router提供下列的几个方法


  • pushUrl -压栈
  • replaceUrl-替换页面栈
  • clear-清空之前页面栈
  • back-返回
  • getLength-获取当前全部的路由长度
  • getParams-获取参数
  • getState-获取当前路由状态
  • 单例模式
  • showAlertBeforeBackPage- (返回阻断)
    :::
  • pushUrl
    :::success
    pushUrl会在当前页面层级再加一层页面,不管是不是同一个页面,
    A -> B 相当于当前页面栈中存在两个页面 A和B
    鸿蒙体系最多页面栈为32,到达32时无法继续push,可以replace(模拟器bug:push到32时replace会表现33,真机不会出现这个题目)

:::
  1. Button('push跳转02')
  2.           .onClick(() => {
  3.             router.pushUrl({
  4.               url: 'pages/10/RouterCase02'
  5.             })
  6.           })
复制代码
:::success
注意跳转的页面必须是Entry修饰的页面
:::


  • replaceUrl
    :::success
  • replaceUrl会替换当前页面,不管是不是同一个页面,替换之后相当于页面重新执行
    :::
  1. Button('replace跳转02')
  2.           .onClick(() => {
  3.             router.replaceUrl({
  4.               url: 'pages/10/RouterCase02'
  5.             })
  6.           })
复制代码


  • clear
    :::success
    清空页面栈中的全部汗青页面,仅保存当前页面作为栈顶页面。
    :::
  1. router.clear()
复制代码


  • back
    :::success
    回到上一个页面- 回到上一个页面,上一个页面并不会重新初始化
    :::
  1. router.back()
复制代码


  • getParams
    :::success
    在跳转过程中,可以给指定页面转达参数,在pushUrl和replaceUrl的第二个参数
    back也可以传参数
    :::
  1. Button('携带参数跳转')
  2.           .onClick(() => {
  3.             router.pushUrl({
  4.               url: 'pages/10/RouterCase02',
  5.               params: {
  6.                 id: 1
  7.               }
  8.             })
  9.           })
复制代码


  • 在吸收页面通过getParams吸收参数
  1. router.getParams()
复制代码
:::success
值得注意的是全部的参数 不论传入和传出都是object,我们需要将其断言成我们想要的范例
:::


  • getState
    :::success
    获取当前页面的状态信息。
    :::
  1. JSON.stringify(router.getState()
复制代码


  • getLength
    :::success
    获取当前页面栈的数目
    :::
  1. JSON.stringify(router.getLength()
复制代码

  1. import { promptAction, router } from '@kit.ArkUI';@Entry@Componentstruct RouterCase01 {  @State message: string = 'RouterCase01';  aboutToAppear(): void {    promptAction.showToast({      message: `页面数目:${router.getLength()}      路由参数:${JSON.stringify(router.getParams()
  2. )}      页面状态:${JSON.stringify(router.getState()
  3. )}`    })  }  build() {    Row() {      Column({ space: 20 }) {        Text(this.message)          .fontSize(50)          .fontWeight(FontWeight.Bold)        Button('push跳转02')
  4.           .onClick(() => {
  5.             router.pushUrl({
  6.               url: 'pages/10/RouterCase02'
  7.             })
  8.           })
  9.         Button('replace跳转02')
  10.           .onClick(() => {
  11.             router.replaceUrl({
  12.               url: 'pages/10/RouterCase02'
  13.             })
  14.           })
  15.         Button('clear清除记录')          .onClick(() => {            router.clear()
  16.             promptAction.showToast({              message: '清空记录'            })          })        Button('back返回')          .onClick(() => {            router.back()
  17.           })        Button('携带参数跳转')
  18.           .onClick(() => {
  19.             router.pushUrl({
  20.               url: 'pages/10/RouterCase02',
  21.               params: {
  22.                 id: 1
  23.               }
  24.             })
  25.           })
  26.       }      .width('100%')    }    .height('100%')  }}
复制代码


  • 单例模式
    :::success
    路由默认属于标准模式
    push就是不停追加,不管你有没有加载这个页面
    单例模式
    比如你加载过A 在栈底放着 再去追加时 会把页面从栈底拿出 放到栈顶
    :::
  • 单例模式不会造成线程的浪费
    :::success
    假设 A-B-C 现在C现在要回到A,此时用push会酿成 A-B-C-A, 用replace会酿成A-B-A, 可以给pushUrl加上单例模式, 酿成 B-C-A, 大概直接用replace酿成 B-A, 大概跳转后clear酿成 A
    :::

  1. router.pushUrl({
  2.               url: 'pages/03/RouterCase'
  3.             }, router.RouterMode.Single)
复制代码


  • showAlertBeforeBackPage
  1. router.showAlertBeforeBackPage({
  2.         message: '确定要退出吗'
  3.       })
复制代码

:::success
该方法只需要在返回之前执行一下即可,建议进入页面前执行


  • 不能获取点击了确定还是取消,由它本身进行处理
  • 不论物理按键还是back都会触发
  • 假如有onBackPress会被取代物理按键的逻辑,不会触发提示
    :::
模块间跳转

:::success
一个项目可能会有很多模块,假如A模块要跳转B模块的一个页面,该怎么跳转
:::
:::success
包的分类


  • hap- 可以有ability,可以有页面,可以有组件,可以有资源
  • hsp- 共享包- 可以实现按需打包
  • har- 静态资源包- 可以实现资源共享
.app 上架包
假如是性能优先 建议使用har包
假如是体积优先 建议使用hsp包
:::


  • 新建一个共享包
    :::success
    阐明

:::
:::success
har包不可新建page页面,
hap包同entry一样,此时我们要建一个share共享包即最终会生成hsp
:::

使用地点跳转


  1. router.pushUrl({
  2.             url: '@bundle:com.itheima.studu_case/library/ets/pages/Index'
  3.           })
复制代码
:::success
@bundle:包名/模块名/ets/pages/xxx
跳转方式
注意:
此时需要使用模拟器,而且需要摆设你要跳转的hsp包才可以


:::
使用路径name跳转(较麻烦)



  • 在想要跳转到的共享包页面里,给@Entry修饰的自界说组件命名并导出,'hsp_test’是自界说的名字
  1. @Entry({ routeName: 'hsp_test' })
  2. @Component
  3. export struct Index {
复制代码


  • 同时需要在当前包引入对于share包的依赖oh-package.json5,demo是自界说的名字
  1. "dependencies": {
  2.     "@ohos/demo": "file:../library"
  3.   }
复制代码


  • 配置成功后需要在跳转的页面中引入命名路由的页面:
  1. import("@ohos/demo/src/main/ets/pages/Index");
复制代码


  • 跳转共享包
  1. Button("NAME模块跳")
  2.         .onClick(() => {
  3.           router.pushNamedRoute({
  4.             name: 'hsp_test'
  5.           })
  6.         })
复制代码
:::success
这种操纵只适合从hap包 -> hsp一次性的跳转,假设有很多个页面都需要这么跳转,还是采用router
否则太过麻烦
:::
完整代码
  1. import { router } from '@kit.ArkUI';
  2. import("@ohos/library/src/main/ets/pages/Index");
  3. @Entry
  4. @Component
  5. struct RouterBundleCase {
  6.   @State message: string = 'RouterBundleCase';
  7.   build() {
  8.     Row() {
  9.       Column() {
  10.         Text('URL模块跳')
  11.           .fontSize(50)
  12.           .fontWeight(FontWeight.Bold)
  13.           .onClick(()=>{
  14.             router.pushUrl({
  15.               url:'@bundle:com.example.harmonyos_next12_base/feature/src/main/ets/pages/Index'
  16.             })
  17.           })
  18.         Text('NAME模块跳')
  19.           .fontSize(50)
  20.           .fontWeight(FontWeight.Bold)
  21.           .onClick(()=>{
  22.             router.pushNamedRoute({
  23.               name:'hsp_test',
  24.               params:{
  25.                 id:123456789
  26.               }
  27.             })
  28.           })
  29.       }
  30.       .width('100%')
  31.     }
  32.     .height('100%')
  33.   }
  34. }
复制代码
生命周期

1. 组件-生命周期

生命周期-官网链接


  • 自界说组件:@Component装饰的UI单位,可以组合多个体系组件实现UI的复用。
  • 页面:即应用的UI页面。可以由一个大概多个自界说组件构成,@Entry装饰的自界说组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。
带@Entry修饰符的组件
:::info
页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:


  • onPageShow:页面每次表现时触发。
  • onPageHide:页面每次隐蔽时触发一次。
  • onBackPress:当用户点击返回按钮时触发。
    :::
    自界说组件生命周期
    :::info
    组件生命周期,即一样寻常用@Component装饰的自界说组件的生命周期,提供以下生命周期接口:
  • aboutToAppear:组件即将出现时回调该接口,具体机遇为在创建自界说组件的新实例后,在执行其build()函数之前执行。
  • aboutToDisappear:在自界说组件即将析构烧毁时执行。
    :::
由于@Entry 也是@Component组件,以是页面组件同时拥有自界说组件的生命周期

在hmlist中测试一下
  1. aboutToAppear() {
  2.     console.log("页面初始化")
  3.   }
  4.   onPageShow() {
  5.     console.log("页面显示")
  6.   }
  7.   onPageHide() {
  8.     console.log("页面隐藏")
  9.   }
  10.   aboutToDisAppear() {
  11.     // 清理定时器
  12.     console.log("页面销毁")
  13.   }
  14.   onBackPress() {
  15.     console.log("后退键")
  16.   }
复制代码
:::success
更多的逻辑会在aboutToAppear中做数据加载
onPageShow也可以做数据加载 分场景
生存场景下- 送菜送外卖的网约车 具偶然效性的业务 需要在onPageShow
偏固定性场景获取一次数据就行
aboutToDisAppear
清算定时任务 。清算监听-线程监听-进程监听
:::

:::info


  • 带@Entry的页面组件 拥有页面进入,页面烧毁,页面表现,页面隐蔽, 页面返回的生命周期
  • @Component自界说组件 拥有 组件进入、组件烧毁生命周期
    :::
  • 在返回页面中,可以进行处理控制是否返回
    :::success
    在onBackPress中
    return true 体现制止返回
    return false 体现允许返回
    :::
  • 在返回时控制返回
  1. onBackPress() {    promptAction.showDialog({      message: '确定要退出吗',      buttons: [{        text: '取消',        color: "black"      },{        text: '确定',        color: "black"      }],    })    .then((result) => {      if(result.index === 1) {        router.back()
  2.       }    })    return true  }
复制代码
:::success
由于没有办法在生命周期中实现async和await,以是先手动让页面不返回,然后再确定是否要返回,假如确定要返回,就用router.back来实现
:::
2. UIAbility-生命周期

UIAbility-生命周期
UIAbility的生命周期包罗Create、Foreground、Background、Destroy四个状态,如下图所示。



  • onCreate
   Ability创建时回调,执行初始化业务逻辑操纵。
  

  • onDestory
   Ability生命周期回调,在烧毁时回调,执行资源清算等操纵。
  

  • onWindowStageCreate
   当WindowStage创建后调用。
  

  • onWindowStageDestory
   当WindowStage烧毁后调用。
  

  • onForeground
   Ability生命周期回调,当应用从后台转到前台时触发。
  

  • onBackground
   Ability生命周期回调,当应用从前台转到后台时触发
  :::info
UIAbility相当于我们应用中的一个任务,我们可以把自己的app想象成一个UIAbility,但是当项目越来越大,
需要扩展和分担业务的时候,可以采取多个
:::
:::info
如何一道面试题拿下面试官:UIAbility生命周期有哪些?
这14个能说多少算多少(需要next权限)
:::
3. Ability跳转

:::success
多个的abiltiy必须建立在hap中
hsp和har均不让建ability
:::
   Stage模子-FA模子
  

  • 模块-hap-hsp -har
  • UIAbility- 项目中默认有一个- 任务窗口-绘制页面
  • Page
  • Component
  比如微信- 聊天-付出
   

  • UIAbility组件是体系调治的根本单位,为应用提供绘制界面的窗口;
  • 一个UIAbility组件中可以通过多个页面来实现一个功能模块;
  • 每一个UIAbility组件实例,都对应于一个近来任务列表中的任务。
  当我们项目中拆解多个任务的时候,可以通过新建多个Ability的方式来进行任务的拆解
   

  • 比如,我们付出的之后想新开一个任务去专门处理这件事,就可以采用拉起一个新的Ability来实现
  

  • 新建一个付出Ablility - PayAbility




  • 新建PayAbility对应的跳转的付出页面 PayIndex.ets
  1. @Entry
  2. @Component
  3. struct PayIndex {
  4.   build() {
  5.     Row() {
  6.       Column({ space: 15 }) {
  7.         Text("支付Ability")
  8.           .fontSize(40)
  9.           .fontColor(Color.Red)
  10.         Button("支付")
  11.           .width('100%')
  12.       }
  13.     }
  14.     .height('100%')
  15.     .padding(20)
  16.   }
  17. }
复制代码



  • 新建一个主页Page- MainPage用来跳转到付出Ability
  1. @Entry
  2. @Component
  3. struct MainPage {
  4.   build() {
  5.     Row() {
  6.       Column({ space: 15 }){
  7.         Text("主Ability")
  8.           .fontSize(50)
  9.         Button("去支付")
  10.           .width('100%')
  11.       }
  12.     }
  13.     .height('100%')
  14.     .padding(20)
  15.   }
  16. }
复制代码

:::info


  • ability的拉起必须通过模拟器-以是把我们主Ability的启动页设置为我们刚刚新建的主页

:::
:::info
接下来,我们点击去付出按钮的时候 要拉起付出PayAbility
我们采用当前Ability的上下文来实现,使用文档链接

:::


  • 使用Context上下文拉起Ability
    :::info
    这里我们需要准备一个参数Want
    :::
  1. let want: Want = {
  2.       'deviceId': '', // deviceId为空表示本设备
  3.       'bundleName': '包名',
  4.       'abilityName': 'abilityName',
  5.   };
复制代码


  • 拉起Ability
  1. let want: Want = {
  2.               'deviceId': '', // deviceId为空表示本设备
  3.               'bundleName': 'com.itheima.studu_case',
  4.               'abilityName': 'PayAbility',
  5.             };
  6.             (getContext() as common.UIAbilityContext)
  7.               .startAbility(want)
复制代码
:::success
假设我们想调用第三方的包可不可以?
答复: 当然可以,我们只需要知道第三方的包名即可
:::
   接下来,我们需要转达参数了
  

  • 我们需要拉起Ability的时候,传已往一个订单id,让付出能够拿到这个id进行付出相关的事宜
  • 传参使用parameters,它是一个object范例,你可以转达你想传的任意数据
  

  • Ablility传参数



  • 在HeimaPay中的HeimaPayAbility使用AppStorage进行吸收并设置
  1. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  2.     AppStorage.setOrCreate<number>("order_id", want.parameters!["order_id"] as number)
  3.     hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  4.   }
  5.   onNewWant(want: Want) {
  6.     const params =  want.parameters as AbilityParams
  7.     AppStorage.Set<number>("order_id", params.order_id)
  8.   }
复制代码
:::info
onNewWant体现当前的PayAbility并未烧毁的情况下 会调用
:::


  • 在PayIndex中使用StoreageProp接受
  1.   @StorageProp('order_id')
  2.   orderId: number = 0
复制代码



  • 执行完副Ability并返回结果
  1.   Button("支付")
  2.           .width('100%')
  3.           .onClick(() => {
  4.             const context =  getContext(this) as common.UIAbilityContext
  5.             context.terminateSelfWithResult({
  6.               resultCode: 1,
  7.               want: {
  8.                 abilityName: 'EntryAbility',
  9.                 bundleName: 'com.itheima.harmonybase',
  10.                 parameters: {
  11.                   paySuccess: true
  12.                 }
  13.               }
  14.             })
  15.           })
复制代码
:::info
值得注意的是:假如我们想要获取副Ability对应的结果,在startAbility的时候需要使用startAbilityForResult来实现
:::
  1. const result =  await (getContext(this) as common.UIAbilityContext).startAbilityForResult({
  2.                 'bundleName': 'com.itheima.harmonybase',
  3.                 'abilityName': 'PayAbility',
  4.                 parameters: {
  5.                   order_id: Date.now()
  6.                 }
  7.               })
  8.               AlertDialog.show({
  9.                 message: JSON.stringify(result)
  10.               })
复制代码

:::info
我们可以根据付出结果进行数据和业务的处理

:::
   界说返回参数的范例
  1. type ResultParams = Record<string, boolean>
复制代码


  • 吸收Ability的返回结果
  1. type ResultParams = Record<string, boolean>
  2. const result =  await (getContext(this) as common.UIAbilityContext).startAbilityForResult({                'bundleName': 'com.itheima.harmonybase',                'abilityName': 'PayAbility',                parameters: {                  order_id: Date.now()                }              })              const params =  result.want?.parameters as ResultParams              if(params.paySuccess) {                promptAction.showToast({ message: '付出成功' })              }else {                promptAction.showToast({ message: '付出失败' })              }
复制代码


使用动画

1. 属性动画

:::success
属性接口(以下简称属性)包含尺寸属性、结构属性、位置属性等多种范例,用于控制组件的举动。针对当前界面上的组件,其部分属性(如位置属性)的变化会引起UI的变化。添加动画可以让属性值从出发点逐渐变化到尽头,从而产生连续的动画效果。根据变化时是否能够添加动画,可以将属性分为可动画属性和不可动画属性。
:::
可动画属性:


  • 体系可动画属性:
    | 分类 | 阐明 |
    | — | — |
    | 结构属性 | 位置、大小、内边距、外边距、对齐方式、权重等。 |
    | 仿射变换 | 平移、旋转、缩放、锚点等。 |
    | 配景 | 配景颜色、配景含糊等。 |
    | 内容 | 文字大小、文字颜色,图片对齐方式、含糊等。 |
    | 前景 | 前景颜色等。 |
    | Overlay | Overlay属性等。 |
    | 外貌 | 透明度、圆角、边框、阴影等。 |
    | … | … |
:::success
属性动画的实现方式有三种


  • animation属性
  • animateTo函数
  • @animator工具类
    :::
  • 使用animateTo函数
    :::success
    animateTo(value: AnimateParam, event: () => void): void
    原理
    通用函数,对闭包前界面和闭包中的状态变量引起的界面之间的差异做动画。支持多次调用,支持嵌套。
    解释: 不论是组件的表现隐蔽还是属性的变化,使用animateTo都可以实现动画
    :::
  1. @Entry
  2. @Component
  3. struct AnimateToCase {
  4.   @State message: string = 'Hello World';
  5.   @State textSize: number = 50
  6.   @State textColor: string = '#000'
  7.   @State textOpacity: number = 1
  8.   build() {
  9.     Row() {
  10.       Column({ space: 20 }) {
  11.         Text(this.message)
  12.           .fontSize(this.textSize)
  13.           .fontWeight(FontWeight.Bold)
  14.           .fontColor(this.textColor)
  15.           .opacity(this.textOpacity)
  16.         Button('隐藏')
  17.           .onClick(() => {
  18.              animateTo({ duration:1000 },()=>{
  19.                this.message = 'World Hello'
  20.                this.textSize = 16
  21.                this.textColor = '#ff4400'
  22.                this.textOpacity = 0
  23.              })
  24.           })
  25.         Button('显示')
  26.           .onClick(() => {
  27.              animateTo({duration:2000},()=>{
  28.                this.message = 'Hello World'
  29.                this.textSize = 50
  30.                this.textColor = '#ff00f0'
  31.                this.textOpacity = 1
  32.              })
  33.           })
  34.       }
  35.       .width('100%')
  36.     }
  37.     .height('100%')
  38.   }
  39. }
复制代码



  • 通过animation属性
    :::success
    识别组件的可动画属性变化,自动添加动画。
    组件的接口调用是从下往上执行,animation只会作用于在其之上的属性调用。
    组件可以根据调用序次对多个属性设置不同的animation。
    :::
  1. @Entry
  2. @Component
  3. struct AnimationCase {
  4.   @State message: string = 'Hello World';
  5.   @State
  6.   textSize :number = 50
  7.   build() {
  8.     Row() {
  9.       Column({space:20}) {
  10.         Text(this.message)
  11.           .fontSize(this.textSize)
  12.           .fontWeight(FontWeight.Bold)
  13.           .animation({
  14.             // 动画时间
  15.             duration:1000,
  16.             // 重复次数,-1代表不重复
  17.             iterations:3,
  18.             // 动画曲线
  19.             curve:Curve.Smooth,
  20.             // 延迟时间
  21.             delay:1000,
  22.             // 播放模式
  23.             playMode:PlayMode.Alternate
  24.           })
  25.         Button('变小')
  26.           .onClick(()=>{
  27.             this.textSize = 16
  28.           })
  29.         Button('变大')
  30.           .onClick(()=>{
  31.             this.textSize = 50
  32.           })
  33.       }
  34.       .width('100%')
  35.     }
  36.     .height('100%')
  37.   }
  38. }
复制代码



  • 通过@animator
    :::info
    之前两种方式都使用于单次执行动画,假如有一个动画需要重复执行,而且还需要开关控制,这种复杂的动画,更适合交给animator类来实现,我们实现一个播放状态CD旋转,暂停状态CD制止旋转的效果
    :::

:::success
animator使用步骤:


  • 1.手动引入animator
  • 2.准备AnimatorOptions的动画参数
  • 3.创建AnimatorResult范例的动画类
  • 4.监听动画的结果更新UI
    :::
  1. // 只能手动引入animator
  2. import animator, { AnimatorOptions, AnimatorResult } from '@ohos.animator'
  3. @Entry
  4. @Component
  5. struct AnimatorClass {
  6.   // 1.准备动画参数
  7.   CDAnimatorOption: AnimatorOptions = {
  8.     duration: 10 * 1000,
  9.     easing: "linear",
  10.     delay: 0,
  11.     fill: "forwards",
  12.     direction: "normal",
  13.     iterations: -1,
  14.     // 上面的参数一个不能少
  15.     // 下面的参数是动画的核心
  16.     // 这里的起始只有一个值,但是你可以自己定义这个值用在哪里,比如我们用在旋转角度
  17.     // 那么起始角度是0
  18.     begin: 0,
  19.     // 那么终止角度是360
  20.     end: 360
  21.   }
  22.   // 2.准备动画类
  23.   CDAnimator: AnimatorResult = animator.create(this.CDAnimatorOption)
  24.   // 3.监听动画的值,动态改变@State的值引起UI更新从而产生动画
  25.   aboutToAppear(): void {
  26.     this.CDAnimator.onframe = (value) => {
  27.       this.rotateAngle = value
  28.     }
  29.   }
  30.   @State
  31.   rotateAngle: number = 0
  32.   @State
  33.   isPlay: boolean = false
  34.   build() {
  35.     Row() {
  36.       Column({ space: 20 }) {
  37.         Image($r('app.media.b'))
  38.           .width(200)
  39.           .aspectRatio(1)
  40.           .borderRadius(100)
  41.           .rotate({
  42.             angle: this.rotateAngle
  43.           })
  44.         Button('播放/暂停')
  45.           .onClick(() => {
  46.             this.isPlay = !this.isPlay
  47.             this.isPlay ? this.CDAnimator.play() : this.CDAnimator.pause()
  48.           })
  49.       }
  50.       .width('100%')
  51.     }
  52.     .height('100%')
  53.   }
  54. }
复制代码
:::info
训练:做一个心跳的案例吧,使用之前的点赞图标
:::

2.图片帧动画

:::success
通过使用ImageAnimator组件实现逐帧播放图片的本领,可以配置需要播放的图片列表,每张图片可以配置时长
帧动画图片.zip
:::
  1. @Entry
  2. @Component
  3. struct ImageAnimatorCase {
  4.   build() {
  5.     Row() {
  6.       Column() {
  7.         ImageAnimator()
  8.           .images(Array.from(Array(37),(item:string,index:number)=>{
  9.             // 图片路径不能含中文
  10.             return {src:`/assets/JDLoading/loading_${index}.png`} as ImageFrameInfo
  11.           }))
  12.           .duration(3000)
  13.           .state(AnimationStatus.Running)
  14.           .fillMode(FillMode.None)
  15.           .iterations(-1)
  16.           // 必须有宽高
  17.           .width(340)
  18.           .aspectRatio(1)
  19.       }
  20.       .width('100%')
  21.     }
  22.     .height('100%')
  23.   }
  24. }
复制代码

:::success
通过state属性可以控制图片的动画的执行方式
AnimationStatus.Initial 初始化 - 不播放动画
AnimationStatus.Running 播放中 - 播放动画
AnimationStatus.Paused 暂停 - 暂停动画至当前帧
生成一个长度为10的数组:
Array(10)
设置数组每一项的内容:
Array.form(Array(10),(item,index)=>{
return ${index}
})
:::
3.转场动画

:::success


  • **共享元素转场 **
  • 出现/消失转场
  • 模态转场 bindSheet 半模态/bindContentCover 全模态
  • 组件内转场 transition属性
  • 页面专场(不保举)
    :::
  • 共享元素转场

:::success
页面间元素共享转场动画实现:sharedTrasition(‘共享标识’)
同一共享标识的组件在页面间切换时会形成动画
:::
页面1跳转页面2共享同一组件
页面1
  1. import { router } from '@kit.ArkUI';
  2. @Entry
  3. @Component
  4. struct SharedElementCase01 {
  5.   @State message: string = 'SharedElementCase01';
  6.   build() {
  7.     Column() {
  8.       Text(this.message)
  9.         .fontSize(50)
  10.         .fontWeight(FontWeight.Bold)
  11.       Image($r('app.media.b'))
  12.         .width(200)
  13.         .sharedTransition('sharedId')
  14.       Button('登录')
  15.         .onClick(() => {
  16.           router.pushUrl({
  17.             url: 'pages/11/SharedElementCase02'
  18.           })
  19.         })
  20.     }
  21.     .width('100%')
  22.     .height('100%')
  23.   }
  24. }
复制代码
页面2
  1. @Entry
  2. @Component
  3. struct SharedElementCase02 {
  4.   @State message: string = 'SharedElementCase02';
  5.   build() {
  6.     Column() {
  7.       Text(this.message)
  8.         .fontSize(50)
  9.         .fontWeight(FontWeight.Bold)
  10.       Image($r('app.media.b'))
  11.         .width(50)
  12.         .position({
  13.           x: 20,
  14.           y: 20
  15.         })
  16.         .sharedTransition('sharedId', {
  17.           duration: 2*1000
  18.         })
  19.     }
  20.     .justifyContent(FlexAlign.Center)
  21.     .width('100%')
  22.     .height('100%')
  23.   }
  24. }
复制代码


  • 出现/消失专场
    :::success
    直接使用animateTo函数即可
    :::
  1. @Entry
  2. @Component
  3. struct ShowOrHideCase {
  4.   @State message: string = 'Hello World';
  5.   @State
  6.   showMessage: boolean = false
  7.   build() {
  8.     Row() {
  9.       Column() {
  10.         Column() {
  11.           if(this.showMessage) {
  12.             Text(this.message)
  13.               .fontSize(50)
  14.               .fontWeight(FontWeight.Bold)
  15.           }
  16.         }
  17.         .height(50)
  18.         Button("显示/隐藏")
  19.           .onClick(() => {
  20.              animateTo({ duration: 1000 },  () => {
  21.               this.showMessage = !this.showMessage
  22.              })
  23.           })
  24.       }
  25.       .width('100%')
  26.     }
  27.     .height('100%')
  28.   }
  29. }
复制代码


  • 模态专场
    :::success
    模态转场是新的界面覆盖在旧的界面上,旧的界面不消失的一种转场方式。
    :::

:::success
和之前选择图片Case使用的效果一样
:::
  1. @Entry
  2. @Component
  3. struct BindSheetCase {
  4.   // 半模态转场显示隐藏控制
  5.   @State isShowSheet: boolean = false;
  6.   // 通过@Builder构建半模态展示界面
  7.   @Builder
  8.   mySheet() {
  9.     Column() {
  10.       Text('我是SheetBuilder')
  11.         .fontSize(30)
  12.     }
  13.     .padding(20)
  14.     .width('100%')
  15.     .height('100%')
  16.     .backgroundColor(Color.White)
  17.   }
  18.   build() {
  19.     Column({ space: 20 }) {
  20.       Text('BindSheetCase')
  21.         .fontSize(28)
  22.         .padding({ top: 30, bottom: 30 })
  23.       Button('打开Sheet')
  24.         .onClick(() => {
  25.           this.isShowSheet = true
  26.         })
  27.     }
  28.     .width('100%')
  29.     .height('100%')
  30.     .backgroundColor('#f5f7f8')
  31.     .bindSheet(this.isShowSheet, this.mySheet(), {
  32.       height: 300,
  33.       // 如果使用内置关闭按钮,手动改变开关
  34.       onDisappear:()=>{
  35.         this.isShowSheet = !this.isShowSheet
  36.       }
  37.     })
  38.   }
  39. }
复制代码

:::success
全模态和半模态弹层使用方式一样,第三个参数可以设置弹层的 modalTransition 表现模式
:::


  • 组件内元素专场transition
    :::success
    组件内转场主要通过transition属性配置转场参数,在组件插入和删除时表现过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。
    :::

:::success
4.0中的我们使用的transitionOption的属性被废弃了,新增了TransitionEffect的属性设置方式
:::




:::success
语法
.transition(TransitionEffect.SLIDE.animation({
duration: 1000
}).combine(TransitionEffect.rotate({
angle: -180
})).combine(TransitionEffect.translate({
x: ‘-100%’
})))
有三种模式可选

:::
  1.   TransitionEffect.translate({x:'-100%'}).animation({duration:2000})
  2.                 .combine(TransitionEffect.rotate({angle:360}).animation({duration:1000}))
复制代码
  1. @Entry@Componentstruct ComAnCase {  @State  showImage: boolean = false  build() {    Row() {      Column({ space: 20 }) {        Column() {          if(this.showImage) {            Image($r("app.media.b"))              .width(100)              .height(100)              .borderRadius(50)              .transition(                TransitionEffect.translate({x:'-100%'}).animation({duration:2000})
  2.                 .combine(TransitionEffect.rotate({angle:360}).animation({duration:1000}))
  3.               )          }        }        .height(100)        Button("表现/隐蔽")          .onClick(() => {            this.showImage = !this.showImage          })      }      .width('100%')    }    .height('100%')  }}
复制代码

:::success
依赖于一个模式才气触发,不如自界说动画灵活,相识即可
:::


  • 页面转场动画(不保举)


使用方法为:
声明转场动画,包含入场离场两个函数,进行样式的控制即可

page1
  1. import { router } from '@kit.ArkUI';
  2. @Entry
  3. @Component
  4. struct PageTransitionPage1 {
  5.   @State message: string = 'PageTransitionPage1';
  6.   build() {
  7.     Row() {
  8.       Column() {
  9.         Text(this.message)
  10.           .fontSize(50)
  11.           .fontWeight(FontWeight.Bold)
  12.         Image($r('app.media.b'))
  13.           .width(200)
  14.           .onClick(()=>{
  15.             router.pushUrl({
  16.               url:'pages/11/PageTransitionPage2'
  17.             })
  18.           })
  19.       }
  20.       .width('100%')
  21.     }
  22.     .height('100%')
  23.   }
  24.   pageTransition() {
  25.     // 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效
  26.     PageTransitionEnter({ type: RouteType.Push, duration: 3000 })
  27.       .slide(SlideEffect.Right)
  28.     // 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效
  29.     PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
  30.       .slide(SlideEffect.Left)
  31.     // 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效
  32.     PageTransitionExit({ type: RouteType.Push, duration: 3000 })
  33.       .slide(SlideEffect.Left)
  34.     // 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效
  35.     PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
  36.       .slide(SlideEffect.Right)
  37.   }
  38. }
复制代码
page2
  1. import { router } from '@kit.ArkUI';@Entry@Componentstruct PageTransitionPage2 {  @State message: string = 'PageTransitionPage2';  build() {    Row() {      Column() {        Text(this.message)          .fontSize(50)          .fontWeight(FontWeight.Bold)          Button('push进入1')          .onClick(()=>{            router.pushUrl({              url:'pages/11/PageTransitionPage1'            })          })        Button('pop进入1')          .onClick(()=>{            router.back()
  2.           })      }      .width('100%')    }    .height('100%')  }}
复制代码
手势处理

:::success
为组件绑定不同范例的手势事件,并设置事件的响应方法。
:::

:::success
一样寻常情况下 使用组件的gesture即可
:::


  • 手势范例

:::success
我们这里学习两个,长按手势宁静移手势
语法
.gesture( LongPressGesture().onAction(() => {}) )
:::


  • 长按手势LongPressGesture
    :::success

:::
:::success
根本上全部的手势都会有这三个事件
:::


  • 实现一个功能-长按语音按钮,表现语音录制框

  1. import { util } from '@kit.ArkTS'
  2. @Entry
  3. @Component
  4. struct GestureCase {
  5.   @State
  6.   showVoice: boolean = false
  7.   @Builder
  8.   getContent() {
  9.     Column() {
  10.       Row() {
  11.         Row() {
  12.           Text("删")
  13.             .fontColor(Color.White)
  14.             .fontSize(30)
  15.         }
  16.         .justifyContent(FlexAlign.Center)
  17.         .width(80)
  18.         .height(80)
  19.         .borderRadius(40)
  20.         .backgroundColor(Color.Gray)
  21.         .rotate({
  22.           angle: -10
  23.         })
  24.         Row() {
  25.           Text("文")
  26.             .fontColor(Color.White)
  27.             .fontSize(30)
  28.         }
  29.         .justifyContent(FlexAlign.Center)
  30.         .width(80)
  31.         .height(80)
  32.         .borderRadius(40)
  33.         .backgroundColor(Color.Gray)
  34.         .rotate({
  35.           angle: 10
  36.         })
  37.       }
  38.       .height(80)
  39.       .width('100%')
  40.       .padding({
  41.         left: 40,
  42.         right: 40
  43.       })
  44.       .justifyContent(FlexAlign.SpaceBetween)
  45.     }
  46.     .justifyContent(FlexAlign.Center)
  47.     .width('100%')
  48.     .height('100%')
  49.     .backgroundColor("rgba(0,0,0,0.4)")
  50.   }
  51.   build() {
  52.     Row() {
  53.       Column() {
  54.         Button("语音")
  55.           .width('100%')
  56.           .type(ButtonType.Normal)
  57.           .gesture(
  58.             LongPressGesture()
  59.               .onAction(() => {
  60.                 this.showVoice = true
  61.               })
  62.               .onActionEnd(() => {
  63.                 this.showVoice = false
  64.               })
  65.           )
  66.       }
  67.       .padding(20)
  68.       .width('100%')
  69.     }
  70.     .height('100%')
  71.     .bindContentCover($$this.showVoice, this.getContent,
  72.       {
  73.         modalTransition: ModalTransition.NONE
  74.       })
  75.   }
  76. }
复制代码


  • 拖动手势PanGesture
    :::success

:::
结合原来的长按,长按基础上,拖动实现删除大概文本按钮的选中
:::success
此时需要使用组合手势,由于是长按和拖动手势的集合
组合手势GestureGroup(mode: GestureMode, …gesture: GestureType[])
GestureMode

GestureEvent的事件参数



:::


  • 手指的坐标信息
    :::success

:::
:::success
判断逻辑,只要发现x坐标在中线偏左,左边就选中,中线偏右右边选中
:::


  • 声明一个罗列范例
  1. enum SelectType {
  2.   DElETE,
  3.   TEXT,
  4.   NONE
  5. }
复制代码


  • 通过onAreaChange的值事件拿宽度
  1. screenWidth: number = 0
  2.   .onAreaChange((oldArea: Area, newArea: Area) => {
  3.       this.screenWidth = newArea.width as number
  4.     })
复制代码


  • 在拖动更新事件中判断坐标落点
  1. PanGesture()
  2.                 .onActionUpdate((event) => {
  3.                   if(event.fingerList[0].globalX < this.screenWidth / 2) {
  4.                     this.currentMode = SelectType.DElETE
  5.                   }else {
  6.                     this.currentMode = SelectType.TEXT
  7.                   }
  8.                 })
  9.                 .onActionEnd(() => {
  10.                   this.currentMode = SelectType.NONE
  11.                 })
复制代码

:::success
完成代码
:::
  1. import { util } from '@kit.ArkTS'import {  deviceInfo } from '@kit.BasicServicesKit'import { display, promptAction } from '@kit.ArkUI'@Entry@Componentstruct GestureCase {  @State  showVoice: boolean = false  screenWidth: number = 0  @State  currentMode: SelectType = SelectType.NONE  @Builder  getContent() {    Column() {      Row() {        Row() {          Text("删")            .fontColor(Color.White)            .fontSize(30)        }        .justifyContent(FlexAlign.Center)        .width(80)        .height(80)        .borderRadius(40)        .backgroundColor(this.currentMode === SelectType.DElETE ? Color.Red  : Color.Gray)        .rotate({          angle: -10        })        Row() {          Text("文")            .fontColor(Color.White)            .fontSize(30)        }        .justifyContent(FlexAlign.Center)        .width(80)        .height(80)        .borderRadius(40)        .backgroundColor(this.currentMode === SelectType.TEXT ? Color.Red  : Color.Gray)        .rotate({          angle: 10        })      }      .height(80)      .width('100%')      .padding({        left: 40,        right: 40      })      .justifyContent(FlexAlign.SpaceBetween)    }    .justifyContent(FlexAlign.Center)    .width('100%')    .height('100%')    .backgroundColor("rgba(0,0,0,0.4)")  }  build() {    Row() {      Column() {        Button("语音")          .width('100%')          .type(ButtonType.Normal)          .gesture(            GestureGroup(GestureMode.Parallel,              LongPressGesture()              .onAction(() => {                this.showVoice = true              })              .onActionEnd(() => {                this.showVoice = false              }),              PanGesture()
  2.                 .onActionUpdate((event) => {
  3.                   if(event.fingerList[0].globalX < this.screenWidth / 2) {
  4.                     this.currentMode = SelectType.DElETE
  5.                   }else {
  6.                     this.currentMode = SelectType.TEXT
  7.                   }
  8.                 })
  9.                 .onActionEnd(() => {
  10.                   this.currentMode = SelectType.NONE
  11.                 })
  12.             )          )      }      .padding(20)      .width('100%')    }    .width('100%')    .height('100%')    .bindContentCover($$this.showVoice, this.getContent,      {        modalTransition: ModalTransition.NONE      })    .onAreaChange((oldArea: Area, newArea: Area) => {      this.screenWidth = newArea.width as number    })  }}enum SelectType {
  13.   DElETE,
  14.   TEXT,
  15.   NONE
  16. }
复制代码
:::success
获取屏幕宽度


  • 页面最外层的组件onAreaChange 拿到最新的宽高
display的本领 需要模拟器
display.getDefaultDisplaySync() 拿到全部展示的屏幕的宽高
:::
沙箱文件操纵

:::success
应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的掩护机制下,应用可见的目录范围即为“应用沙箱目录”。


  • 对于每个应用,体系会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分体系文件(应用运行必需的少量体系文件)地点的目录构成的集合。
  • 应用沙箱限定了应用可见的数据的最小范围。在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的体系文件(应用运行必需的少量体系文件)。因此,本应用的文件也不为其他应用可见,从而掩护了应用文件的安全。
  • 应用可以在“应用文件目录”下保存和处理自己的应用文件;体系文件及其目录对于应用是只读的;而应用若需访问用户文件,则需要通过特定API同时颠末用户的相应授权才气进行。
    :::

:::success
应用文件目录与应用文件路径

如前文所述,“应用沙箱目录”内分为两类:应用文件目录和体系文件目录。
体系文件目录对应用的可见范围由体系预置,开发者无需关注。
在此主要介绍应用文件目录,如下图所示。应用文件目录下某个文件或某个具体目录的路径称为应用文件路径。应用文件目录下的各个文件路径,具备不同的属性和特征。
图3 应用文件目录结构图
:::



  • 获取沙箱目录
    :::success
    getContext().cacheDir
    getContext().fileDir
    getContext().tempDir
    :::
文件操纵

:::success
harmonyOS提供文件操纵的API,相当于nodejs的中的fs操纵
值得注意的是: 在API9中 使用fs
在当前的API11和API12中官方又提供了 fileIO的基础方法,用法和fs根本一致
open 打开文件
close 关闭文件
write写入文件
copy 复制文件
unlink 删除文件
mkdir 创建文件夹
上述方法均支持promise并提供有对应的同步方法
想要操纵一个文件,起首要打开一个文件,读取一个文件的buffer大概fd,通过fd进行文件的buffer进行相应的操纵
:::


  • 试着下载一个图片到我们的沙箱路径,而且表现在页面上(模拟器)
  1. import { request } from '@kit.BasicServicesKit';
  2. @Entry
  3. @Component
  4. struct DownloadCase {
  5.   @State downloadUrl: string = 'https://foruda.gitee.com/avatar/1705232317138324256/1759638_itcast_panpu_1705232317.png';
  6.   @State filePath:string = ''
  7.   build() {
  8.     Row() {
  9.       Column({ space: 20 }) {
  10.         Image(this.downloadUrl)
  11.           .width(200)
  12.         Button('下载')
  13.           .onClick(async () => {
  14.             let filePath = getContext().cacheDir + '/test.jpg'
  15.             const task = await request.downloadFile(getContext(), {
  16.               url: this.downloadUrl,
  17.               filePath:filePath
  18.             })
  19.             task.on('complete', () => {
  20.               this.filePath = filePath
  21.               AlertDialog.show({
  22.                 message: '下载成功'
  23.               })
  24.             })
  25.           })
  26.         if(this.filePath){
  27.           Image('file://'+this.filePath)
  28.             .width(200)
  29.         }
  30.       }
  31.       .width('100%')
  32.     }
  33.     .height('100%')
  34.   }
  35. }
复制代码

:::success
沙箱目录的内容 图片大概web组件要去访问的,需要使用文件协议
file:// 文件协议
http://
https://
:::
混淆开发中的热更新操纵

:::success
Hybrid 混淆开发
原生应用 + web前端
原生壳子webview + SDK
:::
:::success
现在线上有个压缩包,是我们的h5页面,可以正常通过欣赏器访问,我们需要在应用中进行下载解压到我们的沙箱目录下,而且下载完成能够正常访问
:::


  • 准备可访问的网络资源(压缩包)
    :::success
    https://gitee.com/shuiruohanyu/toutiao_net/raw/master/resources/toutiao.zip
    :::
  • 实现下载方法
  1. import { request } from '@kit.BasicServicesKit'
  2. import { fileIo } from '@kit.CoreFileKit'
  3. import { promptAction } from '@kit.ArkUI'
  4. @Entry
  5. @Component
  6. struct HyBridHotLoad {
  7.   @State
  8.   showLoading: boolean = false
  9.   @State
  10.   currentValue: number = 0
  11.   @State
  12.   totalValue: number = 0
  13.   async downLoad() {
  14.     this.showLoading = true
  15.     const fileName = "toutiao.zip"
  16.     // 判断一下 我们的目录是否已经有了这个
  17.     const filePath = getContext().filesDir + '/' + fileName
  18.     // file cache temp
  19.     if (fileIo.listFileSync(getContext().filesDir).includes(fileName)) {
  20.       // 沙箱目录下已经有了这个文件
  21.       // 备份
  22.       fileIo.renameSync(filePath, getContext().filesDir + '/toutiao.bak.zip')
  23.     }
  24.     const task = await request.downloadFile(getContext(), {
  25.       url: 'https://gitee.com/shuiruohanyu/toutiao_net/raw/master/resources/toutiao.zip',
  26.       filePath
  27.     })
  28.     task.on("progress", (current, total) => {
  29.       this.currentValue = current
  30.       this.totalValue = total
  31.     })
  32.     task.on("fail", (error) => {
  33.        AlertDialog.show({ message: error.toString() })
  34.     })
  35.     task.on("complete", () => {
  36.       this.showLoading = false
  37.       promptAction.showToast({ message: '下载成功' })
  38.     })
  39.   }
  40.   @Builder
  41.   getContent() {
  42.     Column() {
  43.       Progress({
  44.         value: this.currentValue,
  45.         total: this.totalValue
  46.       })
  47.         .width('100%')
  48.     }
  49.     .justifyContent(FlexAlign.Center)
  50.     .width('100%')
  51.     .height('100%')
  52.     .backgroundColor("rgba(0,0,0,0.5)")
  53.   }
  54.   build() {
  55.     Row() {
  56.       Column() {
  57.         Button("热更新")
  58.           .onClick(() => {
  59.             this.downLoad()
  60.           })
  61.       }
  62.       .width('100%')
  63.     }
  64.     .height('100%')
  65.     .bindContentCover($$this.showLoading, this.getContent, {
  66.       modalTransition: ModalTransition.NONE
  67.     })
  68.   }
  69. }
复制代码



  • 解压zip包
    :::success
    使用zlib模块

:::
  1. // 解压文件
  2.   async decompressFile () {
  3.     try {
  4.       await zlib.decompressFile(this.filePath, getContext().filesDir)
  5.     }catch(error) {
  6.       AlertDialog.show({
  7.         message: error.message
  8.       })
  9.     }
  10.   }
复制代码



  • 解压后跳转到拥有web组件的页面
  1. // 解压文件
  2.   async decompressFile () {
  3.     try {
  4.       await zlib.decompressFile(this.filePath, getContext().filesDir)
  5.       router.pushUrl({
  6.         url: 'pages/06/WebCase'
  7.       })
  8.     }catch(error) {
  9.       AlertDialog.show({
  10.         message: error.message
  11.       })
  12.     }
  13.   }
复制代码


  • web端页面内容
  1. import { webview } from '@kit.ArkWeb'
  2. interface res {
  3.   url: string;
  4.   message: string;
  5.   result: JsResult;
  6. }
  7. @Entry
  8. @Component
  9. struct WebCase {
  10.   webController: webview.WebviewController = new webview.WebviewController()
  11.   aboutToAppear() {
  12.     // 配置Web开启调试模式
  13.     // .WebviewController.setWebDebuggingAccess(true);
  14.     webview.WebviewController.setWebDebuggingAccess(true);
  15.   }
  16.   build() {
  17.     Column() {
  18.       Web({
  19.         controller: this.webController,
  20.         src: "file://" + getContext().filesDir + '/toutiao/index.html'
  21.       })
  22.         .domStorageAccess(true)
  23.         .width('100%')
  24.         .height("100%")
  25.     }
  26.     .width("100%")
  27.     .height('100%')
  28.   }
  29. }
复制代码
:::success
注意: 由于默认web里面的内容是不开启本地存储的,以是需要使用domStorageAccess属性将允许本地存储的属性开启,否则我们的h5里面的内容就不被允许执行了,会报错
:::

原生本领

1. 音视频播放



  • 视频播放

   arkUI提供了Video组件可以直接播放视频,并提供自带的播放-暂停 全屏,拖动进度等功能
用法
  

  • Video提供构造参数 Video({ src: string | Resource })
  • src支持在线路径-和本地资源路径
  

  • 示例
  1. Video({
  2.           src:'https://video19.ifeng.com/video09/2024/05/23/p7199260608686989961-0-122707.mp4'
  3.         })
复制代码
  版权阐明: 上述代码中的视频链接为参考学习,并非用作贸易用途,请同学们自行放置的外链视频链接
  



  • 放映本地视频
   本地视频我们需要放置在资源目录的原始文件中rawfile目录下,使用$rawfile函数来获取路径进行赋值即可
  1. Video({
  2.             src: $rawfile('travel.mp4')
  3.           })
  4.             .width('100%')
  5.             .aspectRatio(1.4)
复制代码


  • 完整代码
  1. @Entry
  2. @Component
  3. struct VideoCase {
  4.   build() {
  5.     Row() {
  6.       Column() {
  7.        Tabs(){
  8.          TabContent(){
  9.            Video({
  10.              src:'https://video19.ifeng.com/video09/2024/05/23/p7199260608686989961-0-122707.mp4'
  11.            })
  12.          }.tabBar('在线视频')
  13.          TabContent(){
  14.            Video({
  15.              src:$rawfile('p7199260608686989961-0-122707.mp4')
  16.            })
  17.          }.tabBar('本地视频')
  18.        }
  19.       }
  20.       .width('100%')
  21.     }
  22.     .height('100%')
  23.   }
  24. }
复制代码


  • 视频控制-播放-暂停–倍速-全屏-进度
   我们可以通过构造函数传入currentProgressRate 控制倍速,它来自PlaybackSpeed的罗列,现在支持
0.75-1-1.25-1.75-2倍速设置
  

  • 同时我们可以通过传入VideoController来获取视频播放的控制权
  

  • 实现控制倍速播放
  1. @Entry@Componentstruct VideoCase {  @State  speed: number = 1  build() {    Row() {      Tabs() {        TabContent() {          Column({ space: 20 }) {            Video({              currentProgressRate: this.speed,              src: 'https://vd3.bdstatic.com/mda-pmj5ajqd7p4b6pgb/576p/h264/1703044058699262355/mda-pmj5ajqd7p4b6pgb.mp4?auth_key=1703138418-0-0-618ea72b33be241c96c6cff86c06e080&bcevod_channel=searchbox_feed&cr=1&cd=0&pd=1&pt=4&logid=0018430194&vid=9762003448174112444&abtest=all'            })              .width('100%')              .aspectRatio(1.4)            Slider({              value: this.speed,              min: 0.75,              step: 0.25,              max: 2,              style: SliderStyle.InSet            })              .showSteps(true)              .onChange(value => {                this.speed = value              })            Text(this.speed+"倍速").fontSize(14).textAlign(TextAlign.Center).width('100%')          }          .width('100%')        }.tabBar("在线视频")        TabContent() {          Video({
  2.             src: $rawfile('travel.mp4')
  3.           })
  4.             .width('100%')
  5.             .aspectRatio(1.4)
  6.         }        .tabBar("本地视频")      }      .animationDuration(300)    }    .height('100%')  }}
复制代码


  • 实现通过controller控制视频 暂停- 播放-制止-全屏-静音-播放速度- 播放进度

   自界说controller,手动控制视频播放
  1. @Entry
  2. @Component
  3. struct VideoControlCase {
  4.   @State
  5.   currentSpeed: number = 1
  6.   @State
  7.   isMuted: boolean = false
  8.   @State
  9.   showController :boolean = false
  10.   @State
  11.   currentTime: number = 0
  12.   @State
  13.   videoTime: number = 0
  14.   controller: VideoController = new VideoController()
  15.   build() {
  16.     Row() {
  17.       Column({ space: 20 }) {
  18.         Stack({alignContent:Alignment.BottomEnd}){
  19.           Video({
  20.             // 视频源
  21.             src: $rawfile('p7199260608686989961-0-122707.mp4'),
  22.             // 封面图
  23.             previewUri: $r('app.media.b'),
  24.             // 倍速 0.75 ~ 2,0.25一个档
  25.             currentProgressRate: this.currentSpeed,
  26.             // 控制器
  27.             controller: this.controller
  28.           })
  29.             .height(400)
  30.             .objectFit(ImageFit.Contain)// 填充模式
  31.             .autoPlay(true)// 自动播放
  32.             .loop(true)// 循环播放
  33.             .muted(this.isMuted)// 是否静音
  34.             .controls(this.showController) //是否展示控制栏
  35.             .onPrepared((time)=>{// 视频准备好了可以获取视频的时长
  36.               this.videoTime = time.duration
  37.             })
  38.             .onUpdate((time)=>{// 视频播放中可以获取播放的时长
  39.               this.currentTime = time.time
  40.             })
  41.             .onFullscreenChange((screen)=>{// 根据是否全屏判断是否展示控制条
  42.               this.showController = screen.fullscreen
  43.             })
  44.           Row(){
  45.             Button('全屏')
  46.               .onClick(() => {
  47.                 this.controller.requestFullscreen(true)
  48.               })
  49.             // 一般不需要手动全屏,可以过几秒自动退出,提示该充值了!
  50.             // Button('退出全屏')
  51.             //   .onClick(() => {
  52.             //     this.controller.exitFullscreen()
  53.             //   })
  54.           }
  55.         }
  56.         Row({ space: 20 }) {
  57.           Text('播放进度:')
  58.           Slider({
  59.             value: $$this.currentTime,
  60.             min: 0,
  61.             max: this.videoTime,
  62.           })
  63.             .layoutWeight(1)
  64.             // 改变时设置视频播放时长
  65.             .onChange((val) => {
  66.               this.controller.setCurrentTime(val)
  67.             })
  68.         }
  69.         .padding(20)
  70.         Row({ space: 20 }) {
  71.           Text('播放速度:')
  72.           Slider({
  73.             value: $$this.currentSpeed,
  74.             min: 0.75,
  75.             max: 2,
  76.             step: 0.25
  77.           })
  78.             .layoutWeight(1)
  79.         }
  80.         .padding(20)
  81.         Row({ space: 20 }) {
  82.           Button('播放')
  83.             .onClick(() => {
  84.               this.controller.start()
  85.             })
  86.           Button('暂停')
  87.             .onClick(() => {
  88.               this.controller.pause()
  89.             })
  90.           Button('停止')
  91.             .onClick(() => {
  92.               this.controller.stop()
  93.             })
  94.           Button('静音')
  95.             .onClick(() => {
  96.               this.isMuted = !this.isMuted
  97.             })
  98.         }
  99.       }
  100.       .width('100%')
  101.     }
  102.     .height('100%')
  103.   }
  104. }
复制代码
  同理- 假如我们想播放一段音频-用同样的方式给到我们的Video的src属性就可以了Video同时支持
  2. 抖音小案例




  • 声明范例和数据
  1. class VideoItem {
  2.   videoUrl: string = ''
  3.   title: string = ""
  4. }
  5. const allData: VideoItem[] = [
  6.   {
  7.     videoUrl: 'https://vd4.bdstatic.com/mda-pmia5y0htmibjej2/576p/h264/1702970058650094297/mda-pmia5y0htmibjej2.mp4?auth_key=1703155514-0-0-a92de0b6c32239b242d0e51b151ee2d6&bcevod_channel=searchbox_feed&cr=1&cd=0&pd=1&pt=4&logid=2714832517&vid=9811936085320099438&abtest=all',
  8.     title: '我们只是拿某站的数据进行一下测试'
  9.   },
  10.   {
  11.     title: '请大家自行准备在线素材',
  12.     videoUrl: 'https://vd4.bdstatic.com/mda-pmjxx4ccc8x719t3/hd/h264/1703111503445924222/mda-pmjxx4ccc8x719t3.mp4?auth_key=1703155561-0-0-e7c32efbedae026e0e17c900bbd0cf55&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2761194416&vid=7476642150019887968&abtest=all'
  13.   },
  14.   {
  15.     title: '你知道冬天的雪是什么颜色吗, 我猜你不知道',
  16.     videoUrl: 'https://vd4.bdstatic.com/mda-pku9q3zt0rzybip0/hd/cae_h264/1701381974031001593/mda-pku9q3zt0rzybip0.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155589-0-0-133df5be4b625ce34e1a75fe3a4baabf&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2789259407&vid=4775310688475056528&abtest=all'
  17.   },
  18.   {
  19.     title: '宝子们,我当众社死了,我竟然在众目睽睽之下完成了自己人生中的第一段程序',
  20.     videoUrl: 'https://vd2.bdstatic.com/mda-pkkf9qb7zksdaqs9/576p/h264/1700564765354260319/mda-pkkf9qb7zksdaqs9.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155630-0-0-9a47a2910e8d5d90b47ba709fa530b5e&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2830328412&vid=8335346471874826776&abtest=all'
  21.   },
  22.   {
  23.     title: '文学,可以在寂静的夜用曼妙的文字勾勒出关于人生,职场,感情的诸多情绪,无奈此生当为程序员',
  24.     videoUrl: 'https://vd2.bdstatic.com/mda-pj8qa65bc9r1v1cf/576p/h264/1696871444324088416/mda-pj8qa65bc9r1v1cf.mp4?auth_key=1703155654-0-0-fdc0ca9c37ec26be3da9809b89e6151c&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2854467125&vid=5483608480722064830&abtest=all'
  25.   },
  26.   {
  27.     title: '当你阅读到这段文字的时候,我早已入睡,当我在睡梦中惊醒,你却早已安然睡去',
  28.     videoUrl: 'https://vd2.bdstatic.com/mda-pmexhyfui3e6rbmd/hd/cae_h264/1702705379314308540/mda-pmexhyfui3e6rbmd.mp4?auth_key=1703155684-0-0-5b0145fb4c2ec2f0d1bbd525ddb3d592&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2884962294&vid=3059586091403538183&abtest=all'
  29.   },
  30.   {
  31.     title: '每个人的内心都有一段独处的幽闭,不可诉说的窒息感孤独感在每当我们沉静下来的时候变愈发强烈',
  32.     videoUrl: 'https://vd3.bdstatic.com/mda-pmbgjjpkihkf7tjd/576p/h264/1702381478247675613/mda-pmbgjjpkihkf7tjd.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155722-0-0-ea3c2453fbbb2cca66b12e9afe3d419f&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2922207105&vid=9050628586030215591&abtest=all'
  33.   },
  34.   {
  35.     title: '如果在未来的某一天,某一个早晨 晚上 瞬间,你会偶然想起多年前的一段往事,其实并不是我们有多怀旧,只是因为我们走过了太多的路',
  36.     videoUrl: 'https://vd2.bdstatic.com/mda-pj7ktq9euqchetdc/cae_h264/1696824500894354779/mda-pj7ktq9euqchetdc.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155751-0-0-fccb0f110a3b447af67eb0feeabf06ad&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=4&logid=2951492012&vid=12162674818438199896'
  37.   },
  38.   {
  39.     title: '什么是知己,有个网红说,当你理解所有人的时候,你一定不能被所有人理解,每个人都或多或少的自私,只是或多或少而已',
  40.     videoUrl: 'https://vd3.bdstatic.com/mda-pmh5hr95fg6u8u0k/hd/cae_h264/1702877143957184120/mda-pmh5hr95fg6u8u0k.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155785-0-0-5cfc2be95d00306082c7875a747dd998&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2985314718&vid=2720370579167170031&abtest=all'
  41.   }
  42. ]
复制代码


  • 实当代码
  1. @Entry@Componentstruct DouyinCase {  @State  list: VideoItem[] = allData  @State  activeIndex: number = 0  build() {    Swiper() {      // 循环的数据 抖音的列表数据      ForEach(this.list, (item: VideoItem, index: number) => {        // 封装单独的组件实现 Video组件        VideoComp({          item,          index,          activeIndex: this.activeIndex        })      })    }    .index($$this.activeIndex)    .cachedCount(3)    .vertical(true)    .indicator(false)    .width('100%')    .height('100%')  }}@Componentstruct VideoComp {  item: VideoItem = new VideoItem()  index: number = -1  @Require  @Prop  @Watch('changeVideo')  activeIndex: number  changeVideo() {    this.activeIndex === this.index ? this.controller.start() : this.controller.pause()  }  controller: VideoController = new VideoController()  @State  isPlay:boolean = true  build() {    Stack({ alignContent: Alignment.Bottom }) {      Stack(){        Video({          src: this.item.videoUrl,          controller: this.controller        })          .controls(false)          .objectFit(ImageFit.Contain)          .autoPlay(this.activeIndex === this.index ? true : false)          .loop(true)          .onPause(()=>{            this.isPlay = false          })          .onStart(()=>{            this.isPlay = true          })          .onClick(()=>{            this.isPlay?this.controller.pause():this.controller.start()          })        if(!this.isPlay){          Image($r('sys.media.ohos_ic_public_play'))            .width(100)            .aspectRatio(1)            .fillColor('#ccc')            .onClick(()=>{              this.controller.start()            })        }      }      Text(this.item.title)        .fontSize(14)        .fontColor(Color.White)        .padding(20)    }  }}class VideoItem {
  2.   videoUrl: string = ''
  3.   title: string = ""
  4. }
  5. const allData: VideoItem[] = [
  6.   {
  7.     videoUrl: 'https://vd4.bdstatic.com/mda-pmia5y0htmibjej2/576p/h264/1702970058650094297/mda-pmia5y0htmibjej2.mp4?auth_key=1703155514-0-0-a92de0b6c32239b242d0e51b151ee2d6&bcevod_channel=searchbox_feed&cr=1&cd=0&pd=1&pt=4&logid=2714832517&vid=9811936085320099438&abtest=all',
  8.     title: '我们只是拿某站的数据进行一下测试'
  9.   },
  10.   {
  11.     title: '请大家自行准备在线素材',
  12.     videoUrl: 'https://vd4.bdstatic.com/mda-pmjxx4ccc8x719t3/hd/h264/1703111503445924222/mda-pmjxx4ccc8x719t3.mp4?auth_key=1703155561-0-0-e7c32efbedae026e0e17c900bbd0cf55&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2761194416&vid=7476642150019887968&abtest=all'
  13.   },
  14.   {
  15.     title: '你知道冬天的雪是什么颜色吗, 我猜你不知道',
  16.     videoUrl: 'https://vd4.bdstatic.com/mda-pku9q3zt0rzybip0/hd/cae_h264/1701381974031001593/mda-pku9q3zt0rzybip0.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155589-0-0-133df5be4b625ce34e1a75fe3a4baabf&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2789259407&vid=4775310688475056528&abtest=all'
  17.   },
  18.   {
  19.     title: '宝子们,我当众社死了,我竟然在众目睽睽之下完成了自己人生中的第一段程序',
  20.     videoUrl: 'https://vd2.bdstatic.com/mda-pkkf9qb7zksdaqs9/576p/h264/1700564765354260319/mda-pkkf9qb7zksdaqs9.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155630-0-0-9a47a2910e8d5d90b47ba709fa530b5e&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2830328412&vid=8335346471874826776&abtest=all'
  21.   },
  22.   {
  23.     title: '文学,可以在寂静的夜用曼妙的文字勾勒出关于人生,职场,感情的诸多情绪,无奈此生当为程序员',
  24.     videoUrl: 'https://vd2.bdstatic.com/mda-pj8qa65bc9r1v1cf/576p/h264/1696871444324088416/mda-pj8qa65bc9r1v1cf.mp4?auth_key=1703155654-0-0-fdc0ca9c37ec26be3da9809b89e6151c&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2854467125&vid=5483608480722064830&abtest=all'
  25.   },
  26.   {
  27.     title: '当你阅读到这段文字的时候,我早已入睡,当我在睡梦中惊醒,你却早已安然睡去',
  28.     videoUrl: 'https://vd2.bdstatic.com/mda-pmexhyfui3e6rbmd/hd/cae_h264/1702705379314308540/mda-pmexhyfui3e6rbmd.mp4?auth_key=1703155684-0-0-5b0145fb4c2ec2f0d1bbd525ddb3d592&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2884962294&vid=3059586091403538183&abtest=all'
  29.   },
  30.   {
  31.     title: '每个人的内心都有一段独处的幽闭,不可诉说的窒息感孤独感在每当我们沉静下来的时候变愈发强烈',
  32.     videoUrl: 'https://vd3.bdstatic.com/mda-pmbgjjpkihkf7tjd/576p/h264/1702381478247675613/mda-pmbgjjpkihkf7tjd.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155722-0-0-ea3c2453fbbb2cca66b12e9afe3d419f&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2922207105&vid=9050628586030215591&abtest=all'
  33.   },
  34.   {
  35.     title: '如果在未来的某一天,某一个早晨 晚上 瞬间,你会偶然想起多年前的一段往事,其实并不是我们有多怀旧,只是因为我们走过了太多的路',
  36.     videoUrl: 'https://vd2.bdstatic.com/mda-pj7ktq9euqchetdc/cae_h264/1696824500894354779/mda-pj7ktq9euqchetdc.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155751-0-0-fccb0f110a3b447af67eb0feeabf06ad&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=4&logid=2951492012&vid=12162674818438199896'
  37.   },
  38.   {
  39.     title: '什么是知己,有个网红说,当你理解所有人的时候,你一定不能被所有人理解,每个人都或多或少的自私,只是或多或少而已',
  40.     videoUrl: 'https://vd3.bdstatic.com/mda-pmh5hr95fg6u8u0k/hd/cae_h264/1702877143957184120/mda-pmh5hr95fg6u8u0k.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1703155785-0-0-5cfc2be95d00306082c7875a747dd998&bcevod_channel=searchbox_feed&pd=1&cr=1&cd=0&pt=4&logid=2985314718&vid=2720370579167170031&abtest=all'
  41.   }
  42. ]
复制代码
3. 绘画本领-画布组件



   

  • ArkUI里面的画布和前端的Canvas的用法根本一致
  • 使用方法
  1. 1. 放置Canvas组件-给宽和高
复制代码
  1. 2. 初始化画笔对象 CanvasRenderingContext2D,将画笔对象作为构造参数传递给Canvas组件
  2. 3. 可以在Canvas的onReady事件中进行动态绘制
  3. 4. [绘制方法官方文档](https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-canvasrenderingcontext2d-0000001478181441-V3)
复制代码


  • 相识绘画的根本条件:画布、画笔、绘制方法
  1. @Entry
  2. @Component
  3. struct CanvasCase {
  4.   // 2.准备一根笔,传入画布
  5.   myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D()
  6.   drawLine() {
  7.     // moveTo:笔离开画布移动
  8.     this.myPen.moveTo(0, 0)
  9.     // moveTo:笔在画布移动
  10.     this.myPen.lineTo(100, 100)
  11.     // 线宽
  12.     this.myPen.lineWidth = 4
  13.     // 线条颜色
  14.     this.myPen.strokeStyle = 'red'
  15.     // 绘制
  16.     this.myPen.stroke()
  17.   }
  18.   build() {
  19.     Row() {
  20.       Column() {
  21.         // 1.准备一个画布
  22.         Canvas(this.myPen)
  23.           .width('100%')
  24.           .height(300)
  25.           .backgroundColor(Color.Gray)
  26.           .onReady(() => {
  27.             // 3.准备好后就可以进行绘画了
  28.             this.drawLine()
  29.           })
  30.       }
  31.       .width('100%')
  32.     }
  33.     .height('100%')
  34.   }
  35. }
复制代码
绘制其他内容

  1. @Entry
  2. @Component
  3. struct CanvasCase {
  4.   // 2.准备一根笔,传入画布
  5.   myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D()
  6.   @State
  7.   canvasWidth: number = 0
  8.   @State
  9.   canvasHeight: number = 0
  10.   // 清空画布
  11.   drawClear() {
  12.     this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  13.   }
  14.   // 画线
  15.   drawLine() {
  16.     this.myPen.beginPath()
  17.     // moveTo:笔离开画布移动
  18.     this.myPen.moveTo(0, 0)
  19.     // moveTo:笔在画布移动
  20.     this.myPen.lineTo(100, 100)
  21.     // 线宽
  22.     this.myPen.lineWidth = 4
  23.     // 线条颜色
  24.     this.myPen.strokeStyle = 'red'
  25.     // 绘制
  26.     this.myPen.stroke()
  27.     this.myPen.beginPath()
  28.   }
  29.   // 画圆
  30.   drawCircle() {
  31.     this.myPen.beginPath()
  32.     this.myPen.lineWidth = 2
  33.     this.myPen.arc(this.canvasWidth / 2, this.canvasHeight / 2, 100, 0, 360)
  34.     this.myPen.stroke()
  35.     this.myPen.beginPath()
  36.   }
  37.   // 画矩形
  38.   drawRect() {
  39.     this.myPen.beginPath()
  40.     this.myPen.lineWidth = 2
  41.     this.myPen.strokeRect(50, 50, 100, 80)
  42.     // 实心
  43.     // this.myPen.fillRect(50,50,100,80)
  44.     this.myPen.beginPath()
  45.   }
  46.   // 画贝塞尔曲线
  47.   drawBezierCurve() {
  48.     this.myPen.beginPath()
  49.     this.myPen.lineWidth = 2
  50.     this.myPen.moveTo(50, 50)
  51.     this.myPen.bezierCurveTo(100, 233, 30, 327, 111, 343)
  52.     this.myPen.stroke()
  53.     this.myPen.beginPath()
  54.   }
  55.   // 画文字
  56.   drawText(){
  57.     this.myPen.beginPath()
  58.     this.myPen.font = '100px  sans-serif '
  59.     this.myPen.fillText('精忠报国',this.canvasWidth/2,this.canvasHeight/2)
  60.     this.myPen.beginPath()
  61.   }
  62.   //画图
  63.   drawImage(){
  64.     this.myPen.beginPath()
  65.     this.myPen.drawImage(new ImageBitmap('/assets/1.webp'),0,0)
  66.     this.myPen.beginPath()
  67.   }
  68.   build() {
  69.     Column({ space: 15 }) {
  70.       // 1.准备一个画布
  71.       Canvas(this.myPen)
  72.         .width('100%')
  73.         .height(580)
  74.         .backgroundColor(Color.Gray)
  75.         .onReady(() => {
  76.           // 3.准备好后就可以进行绘画了
  77.           // this.drawLine()
  78.         })
  79.         .onAreaChange((_, _val) => {
  80.           this.canvasWidth = _val.width as number
  81.           this.canvasHeight = _val.height as number
  82.         })
  83.       Flex({ wrap: FlexWrap.Wrap }) {
  84.         Button('清空')
  85.           .onClick(() => {
  86.             this.drawClear()
  87.           })
  88.         Button('画线')
  89.           .onClick(() => {
  90.             this.drawLine()
  91.           })
  92.         Button('画圆')
  93.           .onClick(() => {
  94.             this.drawCircle()
  95.           })
  96.         Button('画矩形')
  97.           .onClick(() => {
  98.             this.drawRect()
  99.           })
  100.         Button('画曲线')
  101.           .onClick(() => {
  102.             this.drawBezierCurve()
  103.           })
  104.         Button('画文字')
  105.           .onClick(() => {
  106.             this.drawText()
  107.           })
  108.         Button('画图')
  109.           .onClick(() => {
  110.             this.drawImage()
  111.           })
  112.       }.width('100%')
  113.     }
  114.     .width('100%')
  115.     .height('100%')
  116.   }
  117. }
复制代码
:::success
关于复杂的绘制效果,老潘画了一个地球公转、呆板猫和3D小球,提供代码,感爱好的同学自己模拟创造吧,复杂的动画往往需要复杂的算法和物理知识,比如向心力、能量守恒、摩擦力、加快度、三角函数等等
:::


  • 地球公转

素材:素材.zip
太阳

地球

月球

  1. drawWorld() {
  2.     // 动画:
  3.     // b.找出动画变动的因素
  4.     let earthRotate = 0
  5.     let moonRotate = 0
  6.     // a.画出动画的一帧
  7.     const sun = "/assets/sun.png";
  8.     const moon = "/assets/moon.png";
  9.     const earth = "/assets/earth.png";
  10.     const draw = () => {
  11.       this.myPen.beginPath()
  12.       this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  13.       const r = 100
  14.       const er = 10
  15.       const mr = 5
  16.       // 1.太阳
  17.       this.myPen.restore()
  18.       this.myPen.beginPath()
  19.       this.myPen.drawImage(new ImageBitmap(sun), 0, 0, this.canvasWidth, this.canvasWidth)
  20.       this.myPen.save()
  21.       // 2.轨道
  22.       this.myPen.translate(this.canvasWidth / 2, this.canvasWidth / 2)
  23.       this.myPen.beginPath()
  24.       this.myPen.strokeStyle = 'rgba(0,153,255,0.4)'
  25.       this.myPen.arc(0, 0, r, 0, Math.PI / 180 * 360)
  26.       this.myPen.stroke()
  27.       // 3.地球
  28.       earthRotate += 1
  29.       this.myPen.rotate(Math.PI / 180 * earthRotate)
  30.       this.myPen.save()
  31.       this.myPen.beginPath()
  32.       this.myPen.drawImage(new ImageBitmap(earth), r - er, -er, 2*er, 2*er)
  33.       // 4.月亮
  34.       this.myPen.translate(r, 0)
  35.       moonRotate+=3
  36.       this.myPen.rotate(Math.PI / 180 * moonRotate)
  37.       this.myPen.beginPath()
  38.       this.myPen.drawImage(new ImageBitmap(moon), 2 * er, 0, mr, mr)
  39.       // 5.遮罩层
  40.       this.myPen.restore()
  41.       this.myPen.fillStyle = 'rgba(0,0,0,0.4)'
  42.       this.myPen.fillRect(r, -12, 50, 24)
  43.       this.myPen.closePath()
  44.     }
  45.     setInterval(draw, 30)
  46.   }
复制代码


  • 呆板猫

  1.   drawRobotCat() {
  2.     this.myPen.lineWidth = 2
  3.     const center = [300, 300]
  4.     const r = 240
  5.     //1.以画布中心为圆心,画出头
  6.     this.myPen.arc(center[0], center[1], r, Math.PI / 180 * 135, Math.PI / 180 * 405)
  7.     this.myPen.stroke()
  8.     //2.眼睛
  9.     //  2.1椭圆
  10.     this.myPen.beginPath()
  11.     this.myPen.ellipse(238, 138, 50, 70, Math.PI / 180 * 10, 0, Math.PI / 180 * 360)
  12.     this.myPen.stroke()
  13.     this.myPen.beginPath()
  14.     this.myPen.ellipse(343, 135, 55, 70, Math.PI / 180 * -10, 0, Math.PI / 180 * 360)
  15.     this.myPen.stroke()
  16.     //  2.2折线
  17.     this.myPen.moveTo(267, 116)
  18.     this.myPen.bezierCurveTo(242, 114, 224, 127, 203, 151)
  19.     this.myPen.bezierCurveTo(229, 129, 242, 124, 270, 125)
  20.     this.myPen.bezierCurveTo(243, 123, 214, 142, 210, 158)
  21.     this.myPen.moveTo(317, 116)
  22.     this.myPen.bezierCurveTo(359, 112, 389, 136, 389, 136)
  23.     this.myPen.bezierCurveTo(358, 119, 318, 123, 318, 123)
  24.     this.myPen.bezierCurveTo(362, 112, 384, 140, 384, 140)
  25.     //3.脸
  26.     this.myPen.moveTo(186, 146)
  27.     // 三角函数
  28.     const offset = r * Math.cos(Math.PI / 180 * 45)
  29.     // console.log(center[0]+offset,center[1]+offset);//130,420
  30.     this.myPen.bezierCurveTo(92, 147, 0, 322, center[0] - offset, center[1] + offset)
  31.     this.myPen.moveTo(398, 138)
  32.     this.myPen.bezierCurveTo(576, 138, 550, 402, center[0] + offset, center[1] + offset)
  33.     this.myPen.stroke()
  34.     //4.鼻子
  35.     this.myPen.beginPath()
  36.     this.myPen.moveTo(307, 195)
  37.     this.myPen.arc(285, 195, 25, 0, Math.PI * 360 / 180)
  38.     this.myPen.closePath()
  39.     this.myPen.fill()
  40.     //5.鼻线
  41.     this.myPen.beginPath()
  42.     this.myPen.moveTo(285, 220)
  43.     this.myPen.lineTo(295, 287)
  44.     //6.上嘴角
  45.     this.myPen.moveTo(120, 264)
  46.     this.myPen.bezierCurveTo(100, 233, 30, 327, 111, 343)
  47.     this.myPen.bezierCurveTo(153, 345, 230, 282, 295, 287)
  48.     this.myPen.bezierCurveTo(420, 275, 427, 319, 480, 310)
  49.     this.myPen.bezierCurveTo(529, 304, 518, 239, 460, 245)
  50.     this.myPen.moveTo(111, 343)
  51.     this.myPen.bezierCurveTo(166, 525, 445, 530, 480, 310)
  52.     //7.下嘴巴
  53.     //8.牙齿
  54.     this.myPen.moveTo(160, 340)
  55.     this.myPen.bezierCurveTo(163, 371, 174, 419, 192, 430)
  56.     this.myPen.moveTo(210, 342)
  57.     this.myPen.bezierCurveTo(209, 371, 226, 419, 244, 444)
  58.     this.myPen.moveTo(294, 327)
  59.     this.myPen.lineTo(310, 460)
  60.     this.myPen.moveTo(374, 327)
  61.     this.myPen.bezierCurveTo(390, 355, 387, 407, 370, 434)
  62.     this.myPen.moveTo(435, 335)
  63.     this.myPen.bezierCurveTo(440, 354, 444, 384, 433, 410)
  64.     //9.胡须
  65.     this.myPen.moveTo(169, 162)
  66.     this.myPen.bezierCurveTo(127, 131, 55, 120, 25, 130)
  67.     this.myPen.moveTo(171, 208)
  68.     this.myPen.bezierCurveTo(100, 191, 44, 193, 22, 215)
  69.     this.myPen.moveTo(171, 250)
  70.     this.myPen.bezierCurveTo(110, 251, 50, 271, 20, 310)
  71.     this.myPen.moveTo(412, 162)
  72.     this.myPen.bezierCurveTo(426, 146, 529, 89, 550, 110)
  73.     this.myPen.moveTo(412, 195)
  74.     this.myPen.bezierCurveTo(426, 196, 529, 169, 570, 195)
  75.     this.myPen.moveTo(412, 235)
  76.     this.myPen.bezierCurveTo(426, 236, 529, 239, 580, 260)
  77.     //10.项圈
  78.     this.myPen.moveTo(center[0] - offset, center[1] + offset)
  79.     this.myPen.bezierCurveTo(208, 505, 348, 496, center[0] + offset, center[1] + offset)
  80.     this.myPen.moveTo(center[0] - offset, center[1] + offset)
  81.     this.myPen.bezierCurveTo(center[0] - offset - 10, center[1] + offset + 10, center[0] - offset, center[1] + offset + 20, center[0] - offset, center[1] + offset + 20)
  82.     this.myPen.moveTo(center[0] + offset, center[1] + offset)
  83.     this.myPen.bezierCurveTo(center[0] + offset + 10, center[1] + offset + 10, center[0] + offset, center[1] + offset + 20, center[0] + offset, center[1] + offset + 20)
  84.     this.myPen.moveTo(center[0] - offset, center[1] + offset + 20)
  85.     this.myPen.bezierCurveTo(208, 525, 348, 516, center[0] + offset, center[1] + offset + 20)
  86.     this.myPen.stroke()
  87.   }
复制代码
3D小球:

  1.   draw3DBall(){
  2.     // 设置渐变色
  3.     // 先是居中的光晕
  4.     // 调整光晕
  5.     this.myPen.arc(150,75,50,0,Math.PI/180*360)
  6.     const radialGradient = this.myPen.createRadialGradient(140,70,20,150,75,50)
  7.     radialGradient.addColorStop(0,'#FFE7D8D8')
  8.     radialGradient.addColorStop(1, '#FF6D26E0')
  9.     this.myPen.fillStyle = radialGradient
  10.     this.myPen.shadowOffsetX = 10
  11.     this.myPen.shadowOffsetY = 8
  12.     this.myPen.shadowBlur = 6
  13.     this.myPen.shadowColor = "#993e3535";
  14.     this.myPen.fill()
  15.   }
复制代码
4.签字版


   

  • 接下来需要处理什么时候开始在画板上画的机遇题目了
  • Canvas有一个onTouch事件, 里面包含 按下,抬起,移动等事件,我们以为按下,体现开始画,抬起体现动作结束,移动体现正式绘制,实验用事件来测试一下
  1. Canvas(this.context)
  2.          .width(360)
  3.          .height(300)
  4.          .backgroundColor(Color.Pink)
  5.          .onTouch((event: TouchEvent) => {
  6.            if(event.type === TouchType.Down) {
  7.              promptAction.showToast({ message: '开始绘画' })
  8.            }
  9.            if(event.type === TouchType.Move) {
  10.              promptAction.showToast({ message: '绘画中' })
  11.            }
  12.            if(event.type === TouchType.Up) {
  13.              promptAction.showToast({ message: '结束绘画' })
  14.            }
  15.          })
复制代码


  • 实现绘画
  1.       .onReady(() => {
  2.             this.myPen.lineWidth = 2
  3.             this.myPen.strokeStyle = 'red'
  4.           })
  5.       .onTouch((event) => {
  6.             if (event.type === TouchType.Down) {
  7.               this.myPen.beginPath()
  8.               this.myPen.moveTo(event.touches[0].x, event.touches[0].y)
  9.             } else if (event.type === TouchType.Move) {
  10.               this.myPen.lineTo(event.touches[0].x, event.touches[0].y)
  11.               this.myPen.stroke()
  12.             } else if (event.type === TouchType.Up) {
  13.               this.myPen.closePath()
  14.             }
  15.           })
复制代码
:::success
实现保存图片和清空画布方法,画布的高度需要使用onAreaChange的方式来获取
:::
  1. .onAreaChange((oldArea, newArea) => {
  2.           this.canvasWidth = newArea.width as number
  3.           this.canvasHeight = newArea.height as number
  4.         })
复制代码


  • 按钮调用对应的方法
  1. Row () {
  2.         Button("清空画布")
  3.           .onClick(() => {
  4.             this.clearCanvas()
  5.           })
  6.         Button("保存图片")
  7.           .onClick(() => {
  8.             this.savePicture()
  9.           })
  10.       }
复制代码


  • 清屏方法
  1.   @State
  2.   canvasWidth: number = 0
  3.   @State
  4.   canvasHeight: number = 0
  5.   // 清空画布
  6.   drawClear() {
  7.     this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  8.   }
复制代码


  • 存储图片
   存储图片是将canvas转成base64,可以直接用于展示
  1. Button("存储图片")
  2.           .onClick(() => {
  3.               this.imageUrl = this.context.toDataURL("image/jpg")
  4.           })
复制代码
  也可以将图片写入沙箱后展示,需要将base64 -> buffer

  1. Button("存储图片")
  2.           .onClick(() => {
  3.               // 使用下载到沙箱的图片
  4.               let img = this.myPen.toDataURL('image/jpg')
  5.               const filePath = getContext().tempDir + "/" + Date.now() + '.jpeg'
  6.               const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
  7.               const base64Image = img.split(';base64,').pop();
  8.               // 将base64数据解码为二进制数据
  9.               const imgBuffer = buffer.from(base64Image, "base64")
  10.               fileIo.writeSync(file.fd, imgBuffer.buffer)
  11.               fileIo.closeSync(file)
  12.               this.imageUrl = "file://" + filePath
  13.           })
复制代码
  假如希望手势移动渲染的更加丝滑,可以给画笔加上抗锯齿处理
  1.   context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))
复制代码


  • 完整代码
  1. import { fileIo } from '@kit.CoreFileKit'
  2. import { buffer } from '@kit.ArkTS'
  3. @Entry
  4. @Component
  5. struct SignBoardCase {
  6.   myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))
  7.   @State
  8.   canvasWidth: number = 0
  9.   @State
  10.   canvasHeight: number = 0
  11.   @State
  12.   imageUrl:string = ''
  13.   // 清空画布
  14.   drawClear() {
  15.     this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  16.   }
  17.   build() {
  18.     Row() {
  19.       Column({ space: 20 }) {
  20.         Text('签字板')
  21.         Canvas(this.myPen)
  22.           .width('100%')
  23.           .height(300)
  24.           .backgroundColor(Color.Pink)
  25.           .onReady(() => {
  26.             this.myPen.lineWidth = 2
  27.             this.myPen.strokeStyle = 'red'
  28.           })
  29.           .onAreaChange((_, _val) => {
  30.             this.canvasWidth = _val.width as number
  31.             this.canvasHeight = _val.height as number
  32.           })
  33.           .onTouch((event) => {
  34.             if (event.type === TouchType.Down) {
  35.               this.myPen.beginPath()
  36.               this.myPen.moveTo(event.touches[0].x, event.touches[0].y)
  37.             } else if (event.type === TouchType.Move) {
  38.               this.myPen.lineTo(event.touches[0].x, event.touches[0].y)
  39.               this.myPen.stroke()
  40.             } else if (event.type === TouchType.Up) {
  41.               this.myPen.closePath()
  42.             }
  43.           })
  44.         if(this.imageUrl){
  45.           Image(this.imageUrl)
  46.             .width('100%')
  47.         }
  48.         Row({ space: 20 }) {
  49.           Button('保存')
  50.             .onClick(() => {
  51.               // 使用canvas转化的图片
  52.               // this.imageUrl = this.myPen.toDataURL('image/jpg')
  53.               // 使用下载到沙箱的图片
  54.               let img = this.myPen.toDataURL('image/jpg')
  55.               const filePath = getContext().tempDir + "/" + Date.now() + '.jpeg'
  56.               const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
  57.               const base64Image = img.split(';base64,').pop();
  58.               // 将base64数据解码为二进制数据
  59.               const imgBuffer = buffer.from(base64Image, "base64")
  60.               fileIo.writeSync(file.fd, imgBuffer.buffer)
  61.               fileIo.closeSync(file)
  62.               this.imageUrl = "file://" + filePath
  63.             })
  64.           Button('重签')
  65.             .onClick(() => {
  66.               this.drawClear()
  67.               this.imageUrl = ''
  68.             })
  69.         }
  70.       }
  71.       .width('100%')
  72.     }
  73.     .height('100%')
  74.   }
  75. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

嚴華

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表