134.鸿蒙基础02

沉浸式导航+键盘避让

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

  • 沉浸式导航

准备一张占满整个屏幕的图片,以我的自拍为例,如果你是在预览器里面打开
在这里插入图片描述

这么看没有问题,如果你是在模拟器或者真机打开,你会发现上下两部分被空出来

在这里插入图片描述

:::info
空出来的这两部分叫做安全区,所谓沉浸式指的得就是关闭内置安全区,自己填充安全区的内容
:::

:::success
怎么实现呢?
两种方案

  1. 使用windowStage的设置全屏的方式
  2. 使用组件的安全区域扩展的方式
    :::

使用windowStage来设置

:::success
window非前端window,鸿蒙中属于窗口管理对象,
:::
:::success
在ability中通过getMainWindow可以获取主窗体,然后通过得到的window对象设置全屏即可实现
:::

windowStage.getMainWindow().then(window => {
      window.setWindowLayoutFullScreen(true)
})

在这里插入图片描述在这里插入图片描述

:::success
通过这种方式最简单,但是相当于给所有的页面都设置了沉浸式,如果某些页面不需要设置沉浸式,还需要在页面中通过获取window来关闭
:::
在这里插入图片描述

  • 页面中关闭沉浸式
 aboutToAppear(): void {
    window.getLastWindow(getContext())
      .then(win => {
        win.setWindowLayoutFullScreen(false)
      })
  }

在这里插入图片描述

:::success
还有个问题,如果想要获取安全区域的高度,然后在安全区域做些距离的拉开。
:::
:::success
获取安全区域的高度
getWindowAvoidArea(传入上或者下)
:::

    const win = await window.getLastWindow(getContext())
    // 关闭沉浸式
    win.setWindowLayoutFullScreen(false)
    // 获取上方安全区高度
    this.topSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
      .topRect.height)
    // 获取下方安全区高度
    this.bottomSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
      .bottomRect.height)
    AlertDialog.show({
      message: `
      上安全区:${this.topSafeHeight}
      下安全区:${this.bottomSafeHeight}`
    })

在这里插入图片描述

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

  • 开启沉浸式页面自定义安全区颜色

在这里插入图片描述

import { window } from '@kit.ArkUI'

@Entry
@Component
struct SafeAreaCase {
  @State
  topSafeHeight: number = 0
  @State
  bottomSafeHeight: number = 0

  async aboutToAppear() {
    const win = await window.getLastWindow(getContext())
    // 防止全局没开启,指定页面开启沉浸式
    win.setWindowLayoutFullScreen(true)
    // 获取上方安全区高度
    this.topSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
      .topRect.height)
    // 获取下方安全区高度
    this.bottomSafeHeight = px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
      .bottomRect.height)
    AlertDialog.show({
      message: `
      上安全区:${this.topSafeHeight}
      下安全区:${this.bottomSafeHeight}`
    })
  }

  build() {
    Column() {
      Image($r('app.media.b'))
    }
    .width('100%')
    .height('100%')
    .padding({
      top: this.topSafeHeight,
      bottom: this.bottomSafeHeight
    })
    .backgroundColor(Color.Green)
    .backgroundImageSize({ width: '100%', height: '100%' })
  }
}

安全区域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是个按需的方式,哪个页面需要使用
沉浸式,直接自己设置即可。

  • 作用控制组件扩展其安全区域。
    :::
 Image($r("app.media.handsome"))
        .width('100%')
        .height('50%')
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])

键盘避让模式

:::success
当我们存在输入框的页面,如果点击输入框,此时就会弹出键盘,此时键盘的弹出会出问题,如下图
:::
在这里插入图片描述

@Entry
@Component
struct KeyCase {

  build() {
    Column() {
      Row() {
        Text("顶部内容")
      }
      .justifyContent(FlexAlign.Center)
      .height(50)
      .width('100%')
      Column() {
        Text("中间内容")
      }
      .justifyContent(FlexAlign.Center)
      .backgroundColor(Color.Orange)
      .width('100%')
      .layoutWeight(1)
      Row() {
        TextInput({ placeholder: '请输入内容' })
          .width('100%')
      }
      .padding({
        left: 10,
        right: 10
      })
      .justifyContent(FlexAlign.Center)
      .height(50)
      .width('100%')
    }
    .width('100%')
    .height('100%')
  }
}

在这里插入图片描述

:::success
我们可以设置键盘的避让模式,让窗口被键盘压缩,默认情况下,窗口和键盘的情况是这样的
在这里插入图片描述
设置为压缩就变成
在这里插入图片描述

:::

  • 设置方式 - ability中设置
import {  KeyboardAvoidMode } from '@kit.ArkUI';
windowStage.getMainWindowSync().getUIContext()
        .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的用法)
@Entry
@Component
struct NavigationCase {
  build() {
    // API9
    Navigation() {
      NavRouter() {
        // 只能放一个组件
        Column() {
          Text('A页面的')
          Button('去B页面')
          Button('去B2页面')
        }
        // 第二个会替换第一个,并且点击会进行跳转
        Image($r('app.media.b'))
          .height(100)
        //要跳转的页面
        NavDestination() {
          NavRouter() {
            Column() {
              Text('B页面的')
              Button('去C页面')
            }

            NavDestination() {
              Text('C页面的')
            }
            .title('页面的返回标题')
          }
        }
      }
    }
  }
}

A:在这里插入图片描述

B:在这里插入图片描述

C:在这里插入图片描述

:::success
使用Navigation在不同设备中会得到不同的视觉体验
:::
在这里插入图片描述

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

隐藏标题栏
在这里插入图片描述

自定义返回图标+带参数跳转
!在这里插入图片描述
在这里插入图片描述

不满足条件渲染时显示空白页面
在这里插入图片描述

@Entry
@Component
struct NavigationCase02 {
  // API11
  @Builder
  navDesBuilder(name: string) {
    // 这个builder用于条件渲染展示的页面
    if (name === 'one'){
      // 必须用NavDestination才能显示
      NavDestination() {
        Text('one')
        Button('去TWO')
          .onClick(()=>{
            this.navPathStack.pushPath({
              name:'two'
            })
          })
      }.title('123')
      .hideTitleBar(true)
      // .mode(NavDestinationMode.DIALOG)
    }
    else if (name === 'two') {
      // 可以用组件
      NavigationChild()
    }
  }
  // 1.NavPathStack + navDestination(NavDestination)
  @Provide
  navPathStack: NavPathStack = new NavPathStack()
  build() {
    Navigation(this.navPathStack) {
      // FREE类型滚动时会自动收起
      Scroll(){
        Column({space:20}) {
          Button('下一页')
            .onClick(() => {
              this.navPathStack.pushPath({
                name: 'one'
              })
            })
          Image($r('app.media.b'))
          Image($r('app.media.b'))
          Image($r('app.media.b'))
        }
      }
    }
    .navDestination(this.navDesBuilder)
    .title('西北吴彦祖')
    .titleMode(NavigationTitleMode.Free)
  }
}

@Component
struct NavigationChild {
  @Consume
  navPathStack:NavPathStack
  build() {
    NavDestination() {
      Text('two')
      Button('去THREE')
        .onClick(()=>{
          // 只要跳了,就会有个页面,只不过条件渲染没匹配上,不知道渲染什么
          this.navPathStack.pushPath({
            name:'three'
          })
        })
    }
    .backButtonIcon($r('app.media.a'))
  }
}

@Component
struct NavigationChild2 {
  build() {
    NavDestination() {
      Text('three')
    }
  }
}

:::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,真机不会出现这个问题)
    在这里插入图片描述

:::

 Button('push跳转02')
          .onClick(() => {
            router.pushUrl({
              url: 'pages/10/RouterCase02'
            })
          })

:::success
注意跳转的页面必须是Entry修饰的页面
:::

  • replaceUrl
    :::success

  • replaceUrl会替换当前页面,不管是不是同一个页面,替换之后相当于页面重新执行
    :::

Button('replace跳转02')
          .onClick(() => {
            router.replaceUrl({
              url: 'pages/10/RouterCase02'
            })
          })
  • clear
    :::success
    清空页面栈中的所有历史页面,仅保留当前页面作为栈顶页面。
    :::
router.clear()
  • back
    :::success
    回到上一个页面- 回到上一个页面,上一个页面并不会重新初始化
    :::
 router.back()
  • getParams
    :::success
    在跳转过程中,可以给指定页面传递参数,在pushUrl和replaceUrl的第二个参数
    back也可以传参数
    :::
 Button('携带参数跳转')
          .onClick(() => {
            router.pushUrl({
              url: 'pages/10/RouterCase02',
              params: {
                id: 1
              }
            })
          })
  • 在接收页面通过getParams接收参数
router.getParams()

:::success
值得注意的是所有的参数 不论传入和传出都是object,我们需要将其断言成我们想要的类型
:::

  • getState
    :::success
    获取当前页面的状态信息。
    :::
JSON.stringify(router.getState()
  • getLength
    :::success
    获取当前页面栈的数量
    :::
JSON.stringify(router.getLength()

在这里插入图片描述

import { promptAction, router } from '@kit.ArkUI';

@Entry
@Component
struct RouterCase01 {
  @State message: string = 'RouterCase01';

  aboutToAppear(): void {
    promptAction.showToast({
      message: `页面数量:${router.getLength()}
      路由参数:${JSON.stringify(router.getParams())}
      页面状态:${JSON.stringify(router.getState())}`
    })
  }

  build() {
    Row() {
      Column({ space: 20 }) {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button('push跳转02')
          .onClick(() => {
            router.pushUrl({
              url: 'pages/10/RouterCase02'
            })
          })
        Button('replace跳转02')
          .onClick(() => {
            router.replaceUrl({
              url: 'pages/10/RouterCase02'
            })
          })
        Button('clear清除记录')
          .onClick(() => {
            router.clear()
            promptAction.showToast({
              message: '清空记录'
            })
          })
        Button('back返回')
          .onClick(() => {
            router.back()
          })
        Button('携带参数跳转')
          .onClick(() => {
            router.pushUrl({
              url: 'pages/10/RouterCase02',
              params: {
                id: 1
              }
            })
          })
      }
      .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
    :::
    在这里插入图片描述

 router.pushUrl({
              url: 'pages/03/RouterCase'
            }, router.RouterMode.Single)
  • showAlertBeforeBackPage
 router.showAlertBeforeBackPage({
        message: '确定要退出吗'
      })

在这里插入图片描述

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

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

模块间跳转

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

  • hap- 可以有ability,可以有页面,可以有组件,可以有资源
  • hsp- 共享包- 可以实现按需打包
  • har- 静态资源包- 可以实现资源共享

.app 上架包
如果是性能优先 建议使用har包
如果是体积优先 建议使用hsp包
:::

  • 新建一个共享包
    :::success
    说明
    在这里插入图片描述

:::
:::success
har包不可新建page页面,
hap包同entry一样,此时我们要建一个share共享包即最终会生成hsp
:::
在这里插入图片描述

使用地址跳转

在这里插入图片描述

 router.pushUrl({
            url: '@bundle:com.itheima.studu_case/library/ets/pages/Index'
          })

:::success
@bundle:包名/模块名/ets/pages/xxx
跳转方式
注意:
此时需要使用模拟器,并且需要部署你要跳转的hsp包才可以
在这里插入图片描述

在这里插入图片描述

:::

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

@Entry({ routeName: 'hsp_test' })
@Component
export struct Index {
  • 同时需要在当前包引入对于share包的依赖oh-package.json5,demo是自定义的名字
"dependencies": {
    "@ohos/demo": "file:../library" 
  }
  • 配置成功后需要在跳转的页面中引入命名路由的页面:
import("@ohos/demo/src/main/ets/pages/Index");

  • 跳转共享包
 Button("NAME模块跳")
        .onClick(() => {
          router.pushNamedRoute({
            name: 'hsp_test'
          })
        })

:::success
这种操作只适合从hap包 -> hsp一次性的跳转,假设有很多个页面都需要这么跳转,还是采用router
否则太过麻烦
:::
完整代码

import { router } from '@kit.ArkUI';
import("@ohos/library/src/main/ets/pages/Index");
@Entry
@Component
struct RouterBundleCase {
  @State message: string = 'RouterBundleCase';

  build() {
    Row() {
      Column() {
        Text('URL模块跳')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(()=>{
            router.pushUrl({
              url:'@bundle:com.example.harmonyos_next12_base/feature/src/main/ets/pages/Index'
            })
          })
        Text('NAME模块跳')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(()=>{
            router.pushNamedRoute({
              name:'hsp_test',
              params:{
                id:123456789
              }
            })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

生命周期

1. 组件-生命周期

生命周期-官网链接

  • 自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用。
  • 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

带@Entry修饰符的组件
:::info
页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

  • onPageShow:页面每次显示时触发。

  • onPageHide:页面每次隐藏时触发一次。

  • onBackPress:当用户点击返回按钮时触发。
    :::
    自定义组件生命周期
    :::info
    组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。

  • aboutToDisappear:在自定义组件即将析构销毁时执行。
    :::

因为@Entry 也是@Component组件,所以页面组件同时拥有自定义组件的生命周期
在这里插入图片描述

在hmlist中测试一下

aboutToAppear() {
    console.log("页面初始化")
  }
  onPageShow() {
    console.log("页面显示")
  }
  onPageHide() {
    console.log("页面隐藏")
  }
  aboutToDisAppear() {
    // 清理定时器
    console.log("页面销毁")
  }
  onBackPress() {
    console.log("后退键")
  }

:::success
更多的逻辑会在aboutToAppear中做数据加载
onPageShow也可以做数据加载 分场景
生活场景下- 送菜送外卖的网约车 具有时效性的业务 需要在onPageShow
偏固定性场景获取一次数据就行
aboutToDisAppear
清理定时任务 。清理监听-线程监听-进程监听
:::
在这里插入图片描述

:::info

  • 带@Entry的页面组件 拥有页面进入,页面销毁,页面显示,页面隐藏, 页面返回的生命周期

  • @Component自定义组件 拥有 组件进入、组件销毁生命周期
    :::

  • 在返回页面中,可以进行处理控制是否返回
    :::success
    在onBackPress中
    return true 表示阻止返回
    return false 表示允许返回
    :::

  • 在返回时控制返回

 onBackPress() {
    promptAction.showDialog({
      message: '确定要退出吗',
      buttons: [{
        text: '取消',
        color: "black"
      },{
        text: '确定',
        color: "black"
      }],
    })
    .then((result) => {
      if(result.index === 1) {
        router.back()
      }

    })
    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
@Entry
@Component
struct PayIndex {

  build() {
    Row() {
      Column({ space: 15 }) {
        Text("支付Ability")
          .fontSize(40)
          .fontColor(Color.Red)
        Button("支付")
          .width('100%')
      }

    }
    .height('100%')
    .padding(20)
  }
}

在这里插入图片描述

  • 新建一个主页Page- MainPage用来跳转到支付Ability
@Entry
@Component
struct MainPage {

  build() {
    Row() {
      Column({ space: 15 }){
        Text("主Ability")
          .fontSize(50)
        Button("去支付")
          .width('100%')
      }

    }
    .height('100%')
    .padding(20)
  }
}

在这里插入图片描述

:::info

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

在这里插入图片描述

:::
:::info
接下来,我们点击去支付按钮的时候 要拉起支付PayAbility
我们采用当前Ability的上下文来实现,使用文档链接
在这里插入图片描述

:::

  • 使用Context上下文拉起Ability
    :::info
    这里我们需要准备一个参数Want
    :::
let want: Want = {
      'deviceId': '', // deviceId为空表示本设备
      'bundleName': '包名',
      'abilityName': 'abilityName',
  };
  • 拉起Ability
 let want: Want = {
              'deviceId': '', // deviceId为空表示本设备
              'bundleName': 'com.itheima.studu_case',
              'abilityName': 'PayAbility',
            };
            (getContext() as common.UIAbilityContext)
              .startAbility(want)

:::success
假设我们想调用第三方的包可不可以?
回答: 当然可以,我们只需要知道第三方的包名即可
:::

接下来,我们需要传递参数了

  • 我们需要拉起Ability的时候,传过去一个订单id,让支付能够拿到这个id进行支付相关的事宜
  • 传参使用parameters,它是一个object类型,你可以传递你想传的任意数据
  • Ablility传参数

在这里插入图片描述

  • 在HeimaPay中的HeimaPayAbility使用AppStorage进行接收并设置
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AppStorage.setOrCreate<number>("order_id", want.parameters!["order_id"] as number)
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }
  onNewWant(want: Want) {
    const params =  want.parameters as AbilityParams
    AppStorage.Set<number>("order_id", params.order_id)

  }

:::info
onNewWant表示当前的PayAbility并未销毁的情况下 会调用
:::

  • 在PayIndex中使用StoreageProp接受
  @StorageProp('order_id')
  orderId: number = 0

在这里插入图片描述

  • 执行完副Ability并返回结果
  Button("支付")
          .width('100%')
          .onClick(() => {
            const context =  getContext(this) as common.UIAbilityContext
            context.terminateSelfWithResult({
              resultCode: 1,
              want: {
                abilityName: 'EntryAbility',
                bundleName: 'com.itheima.harmonybase',
                parameters: {
                  paySuccess: true
                }
              }
            })
          })

:::info
值得注意的是:如果我们想要获取副Ability对应的结果,在startAbility的时候需要使用startAbilityForResult来实现
:::

 const result =  await (getContext(this) as common.UIAbilityContext).startAbilityForResult({
                'bundleName': 'com.itheima.harmonybase',
                'abilityName': 'PayAbility',
                parameters: {
                  order_id: Date.now()
                }
              })
              AlertDialog.show({
                message: JSON.stringify(result)
              })

在这里插入图片描述

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

:::

定义返回参数的类型

type ResultParams = Record<string, boolean>

  • 接收Ability的返回结果
type ResultParams = Record<string, boolean>
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都可以实现动画
    :::

@Entry
@Component
struct AnimateToCase {
  @State message: string = 'Hello World';
  @State textSize: number = 50
  @State textColor: string = '#000'
  @State textOpacity: number = 1

  build() {
    Row() {
      Column({ space: 20 }) {
        Text(this.message)
          .fontSize(this.textSize)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.textColor)
          .opacity(this.textOpacity)
        Button('隐藏')
          .onClick(() => {
             animateTo({ duration:1000 },()=>{
               this.message = 'World Hello'
               this.textSize = 16
               this.textColor = '#ff4400'
               this.textOpacity = 0
             })
          })
        Button('显示')
          .onClick(() => {
             animateTo({duration:2000},()=>{
               this.message = 'Hello World'
               this.textSize = 50
               this.textColor = '#ff00f0'
               this.textOpacity = 1
             })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

在这里插入图片描述

  • 通过animation属性
    :::success
    识别组件的可动画属性变化,自动添加动画。
    组件的接口调用是从下往上执行,animation只会作用于在其之上的属性调用。
    组件可以根据调用顺序对多个属性设置不同的animation。
    :::
@Entry
@Component
struct AnimationCase {
  @State message: string = 'Hello World';

  @State
  textSize :number = 50
  build() {
    Row() {
      Column({space:20}) {
        Text(this.message)
          .fontSize(this.textSize)
          .fontWeight(FontWeight.Bold)
          .animation({
            // 动画时间
            duration:1000,
            // 重复次数,-1代表不重复
            iterations:3,
            // 动画曲线
            curve:Curve.Smooth,
            // 延迟时间
            delay:1000,
            // 播放模式
            playMode:PlayMode.Alternate
          })
        Button('变小')
          .onClick(()=>{
            this.textSize = 16
          })
        Button('变大')
          .onClick(()=>{
            this.textSize = 50
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

在这里插入图片描述

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

:::success
animator使用步骤:

  • 1.手动引入animator
  • 2.准备AnimatorOptions的动画参数
  • 3.创建AnimatorResult类型的动画类
  • 4.监听动画的结果更新UI
    :::
// 只能手动引入animator
import animator, { AnimatorOptions, AnimatorResult } from '@ohos.animator'

@Entry
@Component
struct AnimatorClass {
  // 1.准备动画参数
  CDAnimatorOption: AnimatorOptions = {
    duration: 10 * 1000,
    easing: "linear",
    delay: 0,
    fill: "forwards",
    direction: "normal",
    iterations: -1,
    // 上面的参数一个不能少
    // 下面的参数是动画的核心
    // 这里的起始只有一个值,但是你可以自己定义这个值用在哪里,比如我们用在旋转角度
    // 那么起始角度是0
    begin: 0,
    // 那么终止角度是360
    end: 360
  }
  // 2.准备动画类
  CDAnimator: AnimatorResult = animator.create(this.CDAnimatorOption)

  // 3.监听动画的值,动态改变@State的值引起UI更新从而产生动画
  aboutToAppear(): void {
    this.CDAnimator.onframe = (value) => {
      this.rotateAngle = value
    }
  }

  @State
  rotateAngle: number = 0
  @State
  isPlay: boolean = false

  build() {
    Row() {
      Column({ space: 20 }) {
        Image($r('app.media.b'))
          .width(200)
          .aspectRatio(1)
          .borderRadius(100)
          .rotate({
            angle: this.rotateAngle
          })
        Button('播放/暂停')
          .onClick(() => {
            this.isPlay = !this.isPlay
            this.isPlay ? this.CDAnimator.play() : this.CDAnimator.pause()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

:::info
练习:做一个心跳的案例吧,使用之前的点赞图标
:::
在这里插入图片描述

2.图片帧动画

:::success
通过使用ImageAnimator组件实现逐帧播放图片的能力,可以配置需要播放的图片列表,每张图片可以配置时长
帧动画图片.zip
:::

@Entry
@Component
struct ImageAnimatorCase {

  build() {
    Row() {
      Column() {
        ImageAnimator()
          .images(Array.from(Array(37),(item:string,index:number)=>{
            // 图片路径不能含中文
            return {src:`/assets/JDLoading/loading_${index}.png`} as ImageFrameInfo
          }))
          .duration(3000)
          .state(AnimationStatus.Running)
          .fillMode(FillMode.None)
          .iterations(-1)
          // 必须有宽高
          .width(340)
          .aspectRatio(1)
      }
      .width('100%')
    }
    .height('100%')
  }
}

在这里插入图片描述

:::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

import { router } from '@kit.ArkUI';

@Entry
@Component
struct SharedElementCase01 {
  @State message: string = 'SharedElementCase01';

  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Image($r('app.media.b'))
        .width(200)
        .sharedTransition('sharedId')
      Button('登录')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/11/SharedElementCase02'
          })
        })
    }
    .width('100%')
    .height('100%')
  }
}

页面2

@Entry
@Component
struct SharedElementCase02 {
  @State message: string = 'SharedElementCase02';

  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Image($r('app.media.b'))
        .width(50)
        .position({
          x: 20,
          y: 20
        })
        .sharedTransition('sharedId', {
          duration: 2*1000
        })

    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }
}
  • 出现/消失专场
    :::success
    直接使用animateTo函数即可
    :::
@Entry
@Component
struct ShowOrHideCase {
  @State message: string = 'Hello World';
  @State
  showMessage: boolean = false
  build() {
    Row() {
      Column() {
        Column() {
          if(this.showMessage) {
            Text(this.message)
              .fontSize(50)
              .fontWeight(FontWeight.Bold)
          }
        }
        .height(50)

        Button("显示/隐藏")
          .onClick(() => {
             animateTo({ duration: 1000 },  () => {
              this.showMessage = !this.showMessage
             })
          })

      }
      .width('100%')
    }
    .height('100%')
  }
}
  • 模态专场
    :::success
    模态转场是新的界面覆盖在旧的界面上,旧的界面不消失的一种转场方式。
    :::
    在这里插入图片描述

:::success
和之前选择图片Case使用的效果一样
:::

@Entry
@Component
struct BindSheetCase {
  // 半模态转场显示隐藏控制
  @State isShowSheet: boolean = false;

  // 通过@Builder构建半模态展示界面
  @Builder
  mySheet() {
    Column() {
      Text('我是SheetBuilder')
        .fontSize(30)
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }

  build() {
    Column({ space: 20 }) {
      Text('BindSheetCase')
        .fontSize(28)
        .padding({ top: 30, bottom: 30 })
      Button('打开Sheet')
        .onClick(() => {
          this.isShowSheet = true
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f7f8')
    .bindSheet(this.isShowSheet, this.mySheet(), {
      height: 300,
      // 如果使用内置关闭按钮,手动改变开关
      onDisappear:()=>{
        this.isShowSheet = !this.isShowSheet
      }
    })
  }
}

在这里插入图片描述

:::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%’
})))
有三种模式可选
在这里插入图片描述

:::

  TransitionEffect.translate({x:'-100%'}).animation({duration:2000})
                .combine(TransitionEffect.rotate({angle:360}).animation({duration:1000}))
@Entry
@Component
struct 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})
                .combine(TransitionEffect.rotate({angle:360}).animation({duration:1000}))
              )
          }
        }
        .height(100)

        Button("显示/隐藏")
          .onClick(() => {
            this.showImage = !this.showImage
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

在这里插入图片描述

:::success
依赖于一个模式才能触发,不如自定义动画灵活,了解即可
:::

  • 页面转场动画(不推荐)

在这里插入图片描述
在这里插入图片描述

使用方法为:
声明转场动画,包含入场离场两个函数,进行样式的控制即可
在这里插入图片描述

page1

import { router } from '@kit.ArkUI';

@Entry
@Component
struct PageTransitionPage1 {
  @State message: string = 'PageTransitionPage1';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Image($r('app.media.b'))
          .width(200)
          .onClick(()=>{
            router.pushUrl({
              url:'pages/11/PageTransitionPage2'
            })
          })
      }
      .width('100%')
    }
    .height('100%')
  }

  pageTransition() {
    // 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效
    PageTransitionEnter({ type: RouteType.Push, duration: 3000 })
      .slide(SlideEffect.Right)
    // 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效
    PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
      .slide(SlideEffect.Left)
    // 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效
    PageTransitionExit({ type: RouteType.Push, duration: 3000 })
      .slide(SlideEffect.Left)
    // 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效
    PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
      .slide(SlideEffect.Right)
  }
}

page2

import { router } from '@kit.ArkUI';

@Entry
@Component
struct 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()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

手势处理

:::success
为组件绑定不同类型的手势事件,并设置事件的响应方法。
:::
在这里插入图片描述

:::success
一般情况下 使用组件的gesture即可
:::

  • 手势类型
    在这里插入图片描述

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

  • 长按手势LongPressGesture
    :::success
    在这里插入图片描述

:::
:::success
基本上所有的手势都会有这三个事件
:::

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

在这里插入图片描述

import { util } from '@kit.ArkTS'

@Entry
@Component
struct GestureCase {
  @State
  showVoice: boolean = false

  @Builder
  getContent() {
    Column() {
      Row() {
        Row() {
          Text("删")
            .fontColor(Color.White)
            .fontSize(30)
        }
        .justifyContent(FlexAlign.Center)
        .width(80)
        .height(80)
        .borderRadius(40)
        .backgroundColor(Color.Gray)
        .rotate({
          angle: -10
        })
        Row() {
          Text("文")
            .fontColor(Color.White)
            .fontSize(30)
        }
        .justifyContent(FlexAlign.Center)
        .width(80)
        .height(80)
        .borderRadius(40)
        .backgroundColor(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(
            LongPressGesture()
              .onAction(() => {
                this.showVoice = true
              })
              .onActionEnd(() => {
                this.showVoice = false
              })
          )
      }
      .padding(20)
      .width('100%')
    }
    .height('100%')
    .bindContentCover($$this.showVoice, this.getContent,
      {
        modalTransition: ModalTransition.NONE
      })
  }
}
  • 拖动手势PanGesture
    :::success
    在这里插入图片描述

:::
结合原来的长按,长按基础上,拖动实现删除或者文本按钮的选中
:::success
此时需要使用组合手势,因为是长按和拖动手势的集合
组合手势GestureGroup(mode: GestureMode, …gesture: GestureType[])
GestureMode
在这里插入图片描述

GestureEvent的事件参数
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url

在这里插入图片描述

在这里插入图片描述

:::

  • 手指的坐标信息
    :::success
    在这里插入图片描述

:::

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

  • 声明一个枚举类型
enum SelectType {
  DElETE,
  TEXT,
  NONE
}
  • 通过onAreaChange的值事件拿宽度
screenWidth: number = 0

  .onAreaChange((oldArea: Area, newArea: Area) => {
      this.screenWidth = newArea.width as number
    })
  • 在拖动更新事件中判断坐标落点
 PanGesture()
                .onActionUpdate((event) => {
                  if(event.fingerList[0].globalX < this.screenWidth / 2) {
                    this.currentMode = SelectType.DElETE
                  }else {
                    this.currentMode = SelectType.TEXT
                  }
                })
                .onActionEnd(() => {
                  this.currentMode = SelectType.NONE
                })

在这里插入图片描述

:::success
完成代码
:::

import { util } from '@kit.ArkTS'
import {  deviceInfo } from '@kit.BasicServicesKit'
import { display, promptAction } from '@kit.ArkUI'

@Entry
@Component
struct 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()
                .onActionUpdate((event) => {
                  if(event.fingerList[0].globalX < this.screenWidth / 2) {
                    this.currentMode = SelectType.DElETE
                  }else {
                    this.currentMode = SelectType.TEXT
                  }
                })
                .onActionEnd(() => {
                  this.currentMode = SelectType.NONE
                })
            )

          )
      }
      .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 {
  DElETE,
  TEXT,
  NONE
}

:::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进行相应的操作
:::

  • 试着下载一个图片到我们的沙箱路径,并且显示在页面上(模拟器)
import { request } from '@kit.BasicServicesKit';

@Entry
@Component
struct DownloadCase {
  @State downloadUrl: string = 'https://foruda.gitee.com/avatar/1705232317138324256/1759638_itcast_panpu_1705232317.png';
  @State filePath:string = ''
  build() {
    Row() {
      Column({ space: 20 }) {
        Image(this.downloadUrl)
          .width(200)
        Button('下载')
          .onClick(async () => {
            let filePath = getContext().cacheDir + '/test.jpg'
            const task = await request.downloadFile(getContext(), {
              url: this.downloadUrl,
              filePath:filePath
            })
            task.on('complete', () => {
              this.filePath = filePath
              AlertDialog.show({
                message: '下载成功'
              })
            })
          })
        if(this.filePath){
          Image('file://'+this.filePath)
            .width(200)
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}

在这里插入图片描述

:::success
沙箱目录的内容 图片或者web组件要去访问的,需要使用文件协议
file:// 文件协议
http://
https://
:::

混合开发中的热更新操作

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

import { request } from '@kit.BasicServicesKit'
import { fileIo } from '@kit.CoreFileKit'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct HyBridHotLoad {
  @State
  showLoading: boolean = false
  @State
  currentValue: number = 0
  @State
  totalValue: number = 0

  async downLoad() {
    this.showLoading = true
    const fileName = "toutiao.zip"
    // 判断一下 我们的目录是否已经有了这个
    const filePath = getContext().filesDir + '/' + fileName
    // file cache temp
    if (fileIo.listFileSync(getContext().filesDir).includes(fileName)) {
      // 沙箱目录下已经有了这个文件
      // 备份
      fileIo.renameSync(filePath, getContext().filesDir + '/toutiao.bak.zip')
    }
    const task = await request.downloadFile(getContext(), {
      url: 'https://gitee.com/shuiruohanyu/toutiao_net/raw/master/resources/toutiao.zip',
      filePath
    })
    task.on("progress", (current, total) => {
      this.currentValue = current
      this.totalValue = total
    })
    task.on("fail", (error) => {
       AlertDialog.show({ message: error.toString() })
    })
    task.on("complete", () => {
      this.showLoading = false
      promptAction.showToast({ message: '下载成功' })
    })
  }

  @Builder
  getContent() {
    Column() {
      Progress({
        value: this.currentValue,
        total: this.totalValue
      })
        .width('100%')
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .backgroundColor("rgba(0,0,0,0.5)")
  }

  build() {
    Row() {
      Column() {
        Button("热更新")
          .onClick(() => {
            this.downLoad()
          })
      }
      .width('100%')
    }
    .height('100%')
    .bindContentCover($$this.showLoading, this.getContent, {
      modalTransition: ModalTransition.NONE
    })
  }
}

在这里插入图片描述

  • 解压zip包
    :::success
    使用zlib模块
    在这里插入图片描述

:::

// 解压文件
  async decompressFile () {
    try {
      await zlib.decompressFile(this.filePath, getContext().filesDir)
    }catch(error) {
      AlertDialog.show({
        message: error.message
      })
    }
  }

在这里插入图片描述

  • 解压后跳转到拥有web组件的页面
// 解压文件
  async decompressFile () {
    try {
      await zlib.decompressFile(this.filePath, getContext().filesDir)
      router.pushUrl({
        url: 'pages/06/WebCase'
      })
    }catch(error) {
      AlertDialog.show({
        message: error.message
      })
    }
  }
  • web端页面内容
import { webview } from '@kit.ArkWeb'

interface res {
  url: string;
  message: string;
  result: JsResult;
}

@Entry
@Component
struct WebCase {
  webController: webview.WebviewController = new webview.WebviewController()

  aboutToAppear() {
    // 配置Web开启调试模式
    // .WebviewController.setWebDebuggingAccess(true);
    webview.WebviewController.setWebDebuggingAccess(true);
  }

  build() {
    Column() {
      Web({
        controller: this.webController,
        src: "file://" + getContext().filesDir + '/toutiao/index.html'
      })
        .domStorageAccess(true)
        .width('100%')
        .height("100%")
    }
    .width("100%")
    .height('100%')

  }
}

:::success
注意: 因为默认web里面的内容是不开启本地存储的,所以需要使用domStorageAccess属性将允许本地存储的属性开启,否则我们的h5里面的内容就不被允许执行了,会报错
:::
在这里插入图片描述

原生能力

1. 音视频播放

  • 视频播放

在这里插入图片描述

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

  • Video提供构造参数 Video({ src: string | Resource })
  • src支持在线路径-和本地资源路径
  • 示例
 Video({
          src:'https://video19.ifeng.com/video09/2024/05/23/p7199260608686989961-0-122707.mp4'
        })

版权说明: 上述代码中的视频链接为参考学习,并非用作商业用途,请同学们自行放置的外链视频链接

在这里插入图片描述

  • 放映本地视频

本地视频我们需要放置在资源目录的原始文件中rawfile目录下,使用$rawfile函数来获取路径进行赋值即可

 Video({
            src: $rawfile('travel.mp4')
          })
            .width('100%')
            .aspectRatio(1.4)
  • 完整代码
@Entry
@Component
struct VideoCase {
  build() {
    Row() {
      Column() {
       Tabs(){
         TabContent(){
           Video({
             src:'https://video19.ifeng.com/video09/2024/05/23/p7199260608686989961-0-122707.mp4'
           })
         }.tabBar('在线视频')
         TabContent(){
           Video({
             src:$rawfile('p7199260608686989961-0-122707.mp4')
           })
         }.tabBar('本地视频')
       }
      }
      .width('100%')
    }
    .height('100%')
  }
}
  • 视频控制-播放-暂停–倍速-全屏-进度

我们可以通过构造函数传入currentProgressRate 控制倍速,它来自PlaybackSpeed的枚举,目前支持
0.75-1-1.25-1.75-2倍速设置

  • 同时我们可以通过传入VideoController来获取视频播放的控制权
  • 实现控制倍速播放
@Entry
@Component
struct 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({
            src: $rawfile('travel.mp4')
          })
            .width('100%')
            .aspectRatio(1.4)
        }
        .tabBar("本地视频")
      }
      .animationDuration(300)

    }
    .height('100%')
  }
}
  • 实现通过controller控制视频 暂停- 播放-停止-全屏-静音-播放速度- 播放进度

在这里插入图片描述

自定义controller,手动控制视频播放

@Entry
@Component
struct VideoControlCase {
  @State
  currentSpeed: number = 1
  @State
  isMuted: boolean = false
  @State
  showController :boolean = false
  @State
  currentTime: number = 0
  @State
  videoTime: number = 0
  controller: VideoController = new VideoController()

  build() {
    Row() {
      Column({ space: 20 }) {
        Stack({alignContent:Alignment.BottomEnd}){
          Video({
            // 视频源
            src: $rawfile('p7199260608686989961-0-122707.mp4'),
            // 封面图
            previewUri: $r('app.media.b'),
            // 倍速 0.75 ~ 2,0.25一个档
            currentProgressRate: this.currentSpeed,
            // 控制器
            controller: this.controller

          })
            .height(400)
            .objectFit(ImageFit.Contain)// 填充模式
            .autoPlay(true)// 自动播放
            .loop(true)// 循环播放
            .muted(this.isMuted)// 是否静音
            .controls(this.showController) //是否展示控制栏
            .onPrepared((time)=>{// 视频准备好了可以获取视频的时长
              this.videoTime = time.duration
            })
            .onUpdate((time)=>{// 视频播放中可以获取播放的时长
              this.currentTime = time.time
            })
            .onFullscreenChange((screen)=>{// 根据是否全屏判断是否展示控制条
              this.showController = screen.fullscreen
            })
          Row(){
            Button('全屏')
              .onClick(() => {
                this.controller.requestFullscreen(true)
              })
            // 一般不需要手动全屏,可以过几秒自动退出,提示该充值了!
            // Button('退出全屏')
            //   .onClick(() => {
            //     this.controller.exitFullscreen()
            //   })
          }
        }


        Row({ space: 20 }) {
          Text('播放进度:')
          Slider({
            value: $$this.currentTime,
            min: 0,
            max: this.videoTime,
          })
            .layoutWeight(1)
            // 改变时设置视频播放时长
            .onChange((val) => {
              this.controller.setCurrentTime(val)
            })
        }
        .padding(20)

        Row({ space: 20 }) {
          Text('播放速度:')
          Slider({
            value: $$this.currentSpeed,
            min: 0.75,
            max: 2,
            step: 0.25
          })
            .layoutWeight(1)

        }
        .padding(20)

        Row({ space: 20 }) {
          Button('播放')
            .onClick(() => {
              this.controller.start()
            })
          Button('暂停')
            .onClick(() => {
              this.controller.pause()
            })
          Button('停止')
            .onClick(() => {
              this.controller.stop()
            })
          Button('静音')
            .onClick(() => {
              this.isMuted = !this.isMuted
            })
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}

同理- 如果我们想播放一段音频-用同样的方式给到我们的Video的src属性就可以了Video同时支持

2. 抖音小案例

在这里插入图片描述

  • 声明类型和数据
class VideoItem {
  videoUrl: string = ''
  title: string = ""
}

const allData: VideoItem[] = [
  {
    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',
    title: '我们只是拿某站的数据进行一下测试'
  },
  {
    title: '请大家自行准备在线素材',
    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'
  },
  {
    title: '你知道冬天的雪是什么颜色吗, 我猜你不知道',
    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'
  },
  {
    title: '宝子们,我当众社死了,我竟然在众目睽睽之下完成了自己人生中的第一段程序',
    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'
  },
  {
    title: '文学,可以在寂静的夜用曼妙的文字勾勒出关于人生,职场,感情的诸多情绪,无奈此生当为程序员',
    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'
  },
  {
    title: '当你阅读到这段文字的时候,我早已入睡,当我在睡梦中惊醒,你却早已安然睡去',
    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'
  },
  {
    title: '每个人的内心都有一段独处的幽闭,不可诉说的窒息感孤独感在每当我们沉静下来的时候变愈发强烈',
    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'
  },
  {
    title: '如果在未来的某一天,某一个早晨 晚上 瞬间,你会偶然想起多年前的一段往事,其实并不是我们有多怀旧,只是因为我们走过了太多的路',
    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'
  },
  {
    title: '什么是知己,有个网红说,当你理解所有人的时候,你一定不能被所有人理解,每个人都或多或少的自私,只是或多或少而已',
    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'
  }
]
  • 实现代码
@Entry
@Component
struct 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%')
  }
}

@Component
struct 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 {
  videoUrl: string = ''
  title: string = ""
}

const allData: VideoItem[] = [
  {
    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',
    title: '我们只是拿某站的数据进行一下测试'
  },
  {
    title: '请大家自行准备在线素材',
    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'
  },
  {
    title: '你知道冬天的雪是什么颜色吗, 我猜你不知道',
    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'
  },
  {
    title: '宝子们,我当众社死了,我竟然在众目睽睽之下完成了自己人生中的第一段程序',
    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'
  },
  {
    title: '文学,可以在寂静的夜用曼妙的文字勾勒出关于人生,职场,感情的诸多情绪,无奈此生当为程序员',
    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'
  },
  {
    title: '当你阅读到这段文字的时候,我早已入睡,当我在睡梦中惊醒,你却早已安然睡去',
    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'
  },
  {
    title: '每个人的内心都有一段独处的幽闭,不可诉说的窒息感孤独感在每当我们沉静下来的时候变愈发强烈',
    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'
  },
  {
    title: '如果在未来的某一天,某一个早晨 晚上 瞬间,你会偶然想起多年前的一段往事,其实并不是我们有多怀旧,只是因为我们走过了太多的路',
    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'
  },
  {
    title: '什么是知己,有个网红说,当你理解所有人的时候,你一定不能被所有人理解,每个人都或多或少的自私,只是或多或少而已',
    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'
  }
]

3. 绘画能力-画布组件

在这里插入图片描述

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

  drawLine() {
    // moveTo:笔离开画布移动
    this.myPen.moveTo(0, 0)
    // moveTo:笔在画布移动
    this.myPen.lineTo(100, 100)
    // 线宽
    this.myPen.lineWidth = 4
    // 线条颜色
    this.myPen.strokeStyle = 'red'
    // 绘制
    this.myPen.stroke()
  }

  build() {
    Row() {
      Column() {
        // 1.准备一个画布
        Canvas(this.myPen)
          .width('100%')
          .height(300)
          .backgroundColor(Color.Gray)
          .onReady(() => {
            // 3.准备好后就可以进行绘画了
            this.drawLine()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

绘制其他内容
在这里插入图片描述

@Entry
@Component
struct CanvasCase {
  // 2.准备一根笔,传入画布
  myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D()
  @State
  canvasWidth: number = 0
  @State
  canvasHeight: number = 0

  // 清空画布
  drawClear() {
    this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  }

  // 画线
  drawLine() {
    this.myPen.beginPath()
    // moveTo:笔离开画布移动
    this.myPen.moveTo(0, 0)
    // moveTo:笔在画布移动
    this.myPen.lineTo(100, 100)
    // 线宽
    this.myPen.lineWidth = 4
    // 线条颜色
    this.myPen.strokeStyle = 'red'
    // 绘制
    this.myPen.stroke()
    this.myPen.beginPath()
  }

  // 画圆
  drawCircle() {
    this.myPen.beginPath()
    this.myPen.lineWidth = 2
    this.myPen.arc(this.canvasWidth / 2, this.canvasHeight / 2, 100, 0, 360)
    this.myPen.stroke()
    this.myPen.beginPath()
  }

  // 画矩形
  drawRect() {
    this.myPen.beginPath()
    this.myPen.lineWidth = 2
    this.myPen.strokeRect(50, 50, 100, 80)
    // 实心
    // this.myPen.fillRect(50,50,100,80)
    this.myPen.beginPath()
  }

  // 画贝塞尔曲线
  drawBezierCurve() {
    this.myPen.beginPath()
    this.myPen.lineWidth = 2
    this.myPen.moveTo(50, 50)
    this.myPen.bezierCurveTo(100, 233, 30, 327, 111, 343)
    this.myPen.stroke()
    this.myPen.beginPath()
  }

  // 画文字
  drawText(){
    this.myPen.beginPath()
    this.myPen.font = '100px  sans-serif '
    this.myPen.fillText('精忠报国',this.canvasWidth/2,this.canvasHeight/2)
    this.myPen.beginPath()
  }

  //画图
  drawImage(){
    this.myPen.beginPath()
    this.myPen.drawImage(new ImageBitmap('/assets/1.webp'),0,0)
    this.myPen.beginPath()
  }

  build() {
    Column({ space: 15 }) {
      // 1.准备一个画布
      Canvas(this.myPen)
        .width('100%')
        .height(580)
        .backgroundColor(Color.Gray)
        .onReady(() => {
          // 3.准备好后就可以进行绘画了
          // this.drawLine()
        })
        .onAreaChange((_, _val) => {
          this.canvasWidth = _val.width as number
          this.canvasHeight = _val.height as number
        })
      Flex({ wrap: FlexWrap.Wrap }) {
        Button('清空')
          .onClick(() => {
            this.drawClear()
          })
        Button('画线')
          .onClick(() => {
            this.drawLine()
          })
        Button('画圆')
          .onClick(() => {
            this.drawCircle()
          })
        Button('画矩形')
          .onClick(() => {
            this.drawRect()
          })
        Button('画曲线')
          .onClick(() => {
            this.drawBezierCurve()
          })
        Button('画文字')
          .onClick(() => {
            this.drawText()
          })
        Button('画图')
          .onClick(() => {
            this.drawImage()
          })
      }.width('100%')
    }
    .width('100%')
    .height('100%')
  }
}

:::success
关于复杂的绘制效果,老潘画了一个地球公转、机器猫和3D小球,提供代码,感兴趣的同学自己模仿创造吧,复杂的动画往往需要复杂的算法和物理知识,比如向心力、能量守恒、摩擦力、加速度、三角函数等等
:::

  • 地球公转

在这里插入图片描述

素材:素材.zip
太阳 在这里插入图片描述
地球 在这里插入图片描述
月球 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

drawWorld() {
    // 动画:
    // b.找出动画变动的因素
    let earthRotate = 0
    let moonRotate = 0
    // a.画出动画的一帧
    const sun = "/assets/sun.png";
    const moon = "/assets/moon.png";
    const earth = "/assets/earth.png";
    const draw = () => {
      this.myPen.beginPath()
      this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
      const r = 100
      const er = 10
      const mr = 5
      // 1.太阳
      this.myPen.restore()
      this.myPen.beginPath()
      this.myPen.drawImage(new ImageBitmap(sun), 0, 0, this.canvasWidth, this.canvasWidth)
      this.myPen.save()
      // 2.轨道
      this.myPen.translate(this.canvasWidth / 2, this.canvasWidth / 2)
      this.myPen.beginPath()
      this.myPen.strokeStyle = 'rgba(0,153,255,0.4)'
      this.myPen.arc(0, 0, r, 0, Math.PI / 180 * 360)
      this.myPen.stroke()
      // 3.地球
      earthRotate += 1
      this.myPen.rotate(Math.PI / 180 * earthRotate)
      this.myPen.save()
      this.myPen.beginPath()
      this.myPen.drawImage(new ImageBitmap(earth), r - er, -er, 2*er, 2*er)
      // 4.月亮
      this.myPen.translate(r, 0)
      moonRotate+=3
      this.myPen.rotate(Math.PI / 180 * moonRotate)
      this.myPen.beginPath()
      this.myPen.drawImage(new ImageBitmap(moon), 2 * er, 0, mr, mr)

      // 5.遮罩层
      this.myPen.restore()
      this.myPen.fillStyle = 'rgba(0,0,0,0.4)'
      this.myPen.fillRect(r, -12, 50, 24)
      this.myPen.closePath()
    }
    setInterval(draw, 30)
  }
  • 机器猫

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  drawRobotCat() {
    this.myPen.lineWidth = 2
    const center = [300, 300]
    const r = 240
    //1.以画布中心为圆心,画出头
    this.myPen.arc(center[0], center[1], r, Math.PI / 180 * 135, Math.PI / 180 * 405)
    this.myPen.stroke()
    //2.眼睛
    //  2.1椭圆
    this.myPen.beginPath()
    this.myPen.ellipse(238, 138, 50, 70, Math.PI / 180 * 10, 0, Math.PI / 180 * 360)
    this.myPen.stroke()
    this.myPen.beginPath()
    this.myPen.ellipse(343, 135, 55, 70, Math.PI / 180 * -10, 0, Math.PI / 180 * 360)
    this.myPen.stroke()
    //  2.2折线
    this.myPen.moveTo(267, 116)
    this.myPen.bezierCurveTo(242, 114, 224, 127, 203, 151)
    this.myPen.bezierCurveTo(229, 129, 242, 124, 270, 125)
    this.myPen.bezierCurveTo(243, 123, 214, 142, 210, 158)
    this.myPen.moveTo(317, 116)
    this.myPen.bezierCurveTo(359, 112, 389, 136, 389, 136)
    this.myPen.bezierCurveTo(358, 119, 318, 123, 318, 123)
    this.myPen.bezierCurveTo(362, 112, 384, 140, 384, 140)
    //3.脸
    this.myPen.moveTo(186, 146)
    // 三角函数
    const offset = r * Math.cos(Math.PI / 180 * 45)
    // console.log(center[0]+offset,center[1]+offset);//130,420
    this.myPen.bezierCurveTo(92, 147, 0, 322, center[0] - offset, center[1] + offset)
    this.myPen.moveTo(398, 138)
    this.myPen.bezierCurveTo(576, 138, 550, 402, center[0] + offset, center[1] + offset)
    this.myPen.stroke()
    //4.鼻子
    this.myPen.beginPath()
    this.myPen.moveTo(307, 195)
    this.myPen.arc(285, 195, 25, 0, Math.PI * 360 / 180)
    this.myPen.closePath()
    this.myPen.fill()
    //5.鼻线
    this.myPen.beginPath()
    this.myPen.moveTo(285, 220)
    this.myPen.lineTo(295, 287)
    //6.上嘴角
    this.myPen.moveTo(120, 264)
    this.myPen.bezierCurveTo(100, 233, 30, 327, 111, 343)
    this.myPen.bezierCurveTo(153, 345, 230, 282, 295, 287)
    this.myPen.bezierCurveTo(420, 275, 427, 319, 480, 310)
    this.myPen.bezierCurveTo(529, 304, 518, 239, 460, 245)
    this.myPen.moveTo(111, 343)
    this.myPen.bezierCurveTo(166, 525, 445, 530, 480, 310)
    //7.下嘴巴
    //8.牙齿
    this.myPen.moveTo(160, 340)
    this.myPen.bezierCurveTo(163, 371, 174, 419, 192, 430)
    this.myPen.moveTo(210, 342)
    this.myPen.bezierCurveTo(209, 371, 226, 419, 244, 444)
    this.myPen.moveTo(294, 327)
    this.myPen.lineTo(310, 460)
    this.myPen.moveTo(374, 327)
    this.myPen.bezierCurveTo(390, 355, 387, 407, 370, 434)
    this.myPen.moveTo(435, 335)
    this.myPen.bezierCurveTo(440, 354, 444, 384, 433, 410)

    //9.胡须
    this.myPen.moveTo(169, 162)
    this.myPen.bezierCurveTo(127, 131, 55, 120, 25, 130)
    this.myPen.moveTo(171, 208)
    this.myPen.bezierCurveTo(100, 191, 44, 193, 22, 215)
    this.myPen.moveTo(171, 250)
    this.myPen.bezierCurveTo(110, 251, 50, 271, 20, 310)
    this.myPen.moveTo(412, 162)
    this.myPen.bezierCurveTo(426, 146, 529, 89, 550, 110)
    this.myPen.moveTo(412, 195)
    this.myPen.bezierCurveTo(426, 196, 529, 169, 570, 195)
    this.myPen.moveTo(412, 235)
    this.myPen.bezierCurveTo(426, 236, 529, 239, 580, 260)
    //10.项圈
    this.myPen.moveTo(center[0] - offset, center[1] + offset)
    this.myPen.bezierCurveTo(208, 505, 348, 496, center[0] + offset, center[1] + offset)
    this.myPen.moveTo(center[0] - offset, center[1] + offset)
    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)
    this.myPen.moveTo(center[0] + offset, center[1] + offset)
    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)
    this.myPen.moveTo(center[0] - offset, center[1] + offset + 20)
    this.myPen.bezierCurveTo(208, 525, 348, 516, center[0] + offset, center[1] + offset + 20)
    this.myPen.stroke()
  }

3D小球:
在这里插入图片描述

  draw3DBall(){
    // 设置渐变色
    // 先是居中的光晕
    // 调整光晕
    this.myPen.arc(150,75,50,0,Math.PI/180*360)
    const radialGradient = this.myPen.createRadialGradient(140,70,20,150,75,50)
    radialGradient.addColorStop(0,'#FFE7D8D8')
    radialGradient.addColorStop(1, '#FF6D26E0')
    this.myPen.fillStyle = radialGradient
    this.myPen.shadowOffsetX = 10
    this.myPen.shadowOffsetY = 8
    this.myPen.shadowBlur = 6
    this.myPen.shadowColor = "#993e3535";
    this.myPen.fill()
  }

4.签字版

在这里插入图片描述

  • 接下来需要处理什么时候开始在画板上画的时机问题了
  • Canvas有一个onTouch事件, 里面包含 按下,抬起,移动等事件,我们认为按下,表示开始画,抬起表示动作结束,移动表示正式绘制,尝试用事件来测试一下
Canvas(this.context)
         .width(360)
         .height(300)
         .backgroundColor(Color.Pink)
         .onTouch((event: TouchEvent) => {
           if(event.type === TouchType.Down) {
             promptAction.showToast({ message: '开始绘画' })
           }
           if(event.type === TouchType.Move) {
             promptAction.showToast({ message: '绘画中' })
           }
           if(event.type === TouchType.Up) {
             promptAction.showToast({ message: '结束绘画' })
           }
         })
  • 实现绘画
      .onReady(() => {
            this.myPen.lineWidth = 2
            this.myPen.strokeStyle = 'red'
          })
      .onTouch((event) => {
            if (event.type === TouchType.Down) {
              this.myPen.beginPath()
              this.myPen.moveTo(event.touches[0].x, event.touches[0].y)
            } else if (event.type === TouchType.Move) {
              this.myPen.lineTo(event.touches[0].x, event.touches[0].y)
              this.myPen.stroke()
            } else if (event.type === TouchType.Up) {
              this.myPen.closePath()
            }
          })

:::success
实现保存图片和清空画布方法,画布的高度需要使用onAreaChange的方式来获取
:::

 .onAreaChange((oldArea, newArea) => {
          this.canvasWidth = newArea.width as number
          this.canvasHeight = newArea.height as number
        })
  • 按钮调用对应的方法
 Row () {
        Button("清空画布")
          .onClick(() => {
            this.clearCanvas()
          })
        Button("保存图片")
          .onClick(() => {
            this.savePicture()
          })
      }
  • 清屏方法
  @State
  canvasWidth: number = 0
  @State
  canvasHeight: number = 0
  // 清空画布
  drawClear() {
    this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  }
  • 存储图片

存储图片是将canvas转成base64,可以直接用于展示

 Button("存储图片")
          .onClick(() => {
              this.imageUrl = this.context.toDataURL("image/jpg")
          })

也可以将图片写入沙箱后展示,需要将base64 -> buffer
在这里插入图片描述

 Button("存储图片")
          .onClick(() => {
              // 使用下载到沙箱的图片
              let img = this.myPen.toDataURL('image/jpg')
              const filePath = getContext().tempDir + "/" + Date.now() + '.jpeg'
              const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
              const base64Image = img.split(';base64,').pop();
              // 将base64数据解码为二进制数据
              const imgBuffer = buffer.from(base64Image, "base64")
              fileIo.writeSync(file.fd, imgBuffer.buffer)
              fileIo.closeSync(file)
              this.imageUrl = "file://" + filePath
          })

如果希望手势移动渲染的更加丝滑,可以给画笔加上抗锯齿处理

  context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))
  • 完整代码
import { fileIo } from '@kit.CoreFileKit'
import { buffer } from '@kit.ArkTS'

@Entry
@Component
struct SignBoardCase {
  myPen: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))
  @State
  canvasWidth: number = 0
  @State
  canvasHeight: number = 0

  @State
  imageUrl:string = ''
  // 清空画布
  drawClear() {
    this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  }

  build() {
    Row() {
      Column({ space: 20 }) {
        Text('签字板')
        Canvas(this.myPen)
          .width('100%')
          .height(300)
          .backgroundColor(Color.Pink)
          .onReady(() => {
            this.myPen.lineWidth = 2
            this.myPen.strokeStyle = 'red'
          })
          .onAreaChange((_, _val) => {
            this.canvasWidth = _val.width as number
            this.canvasHeight = _val.height as number
          })
          .onTouch((event) => {
            if (event.type === TouchType.Down) {
              this.myPen.beginPath()
              this.myPen.moveTo(event.touches[0].x, event.touches[0].y)
            } else if (event.type === TouchType.Move) {
              this.myPen.lineTo(event.touches[0].x, event.touches[0].y)
              this.myPen.stroke()
            } else if (event.type === TouchType.Up) {
              this.myPen.closePath()
            }
          })
        if(this.imageUrl){
          Image(this.imageUrl)
            .width('100%')
        }
        Row({ space: 20 }) {
          Button('保存')
            .onClick(() => {
              // 使用canvas转化的图片
              // this.imageUrl = this.myPen.toDataURL('image/jpg')
              // 使用下载到沙箱的图片
              let img = this.myPen.toDataURL('image/jpg')
              const filePath = getContext().tempDir + "/" + Date.now() + '.jpeg'
              const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
              const base64Image = img.split(';base64,').pop();
              // 将base64数据解码为二进制数据
              const imgBuffer = buffer.from(base64Image, "base64")
              fileIo.writeSync(file.fd, imgBuffer.buffer)
              fileIo.closeSync(file)
              this.imageUrl = "file://" + filePath
            })
          Button('重签')
            .onClick(() => {
              this.drawClear()
              this.imageUrl = ''
            })
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值