沉浸式导航+键盘避让
:::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对象设置全屏即可实现
:::
- 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
我们可以设置键盘的避让模式,让窗口被键盘压缩,默认情况下,窗口和键盘的情况是如许的
设置为压缩就酿成
:::
- 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
清空页面栈中的全部汗青页面,仅保存当前页面作为栈顶页面。
:::
- back
:::success
回到上一个页面- 回到上一个页面,上一个页面并不会重新初始化
:::
- getParams
:::success
在跳转过程中,可以给指定页面转达参数,在pushUrl和replaceUrl的第二个参数
back也可以传参数
:::
- Button('携带参数跳转')
- .onClick(() => {
- router.pushUrl({
- url: 'pages/10/RouterCase02',
- params: {
- id: 1
- }
- })
- })
复制代码
:::success
值得注意的是全部的参数 不论传入和传出都是object,我们需要将其断言成我们想要的范例
:::
- getState
:::success
获取当前页面的状态信息。
:::
- JSON.stringify(router.getState()
复制代码
- getLength
:::success
获取当前页面栈的数目
:::
- JSON.stringify(router.getLength()
复制代码
- import { promptAction, router } from '@kit.ArkUI';@Entry@Componentstruct 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)
复制代码
- router.showAlertBeforeBackPage({
- message: '确定要退出吗'
- })
复制代码
:::success
该方法只需要在返回之前执行一下即可,建议进入页面前执行
- 不能获取点击了确定还是取消,由它本身进行处理
- 不论物理按键还是back都会触发
- 假如有onBackPress会被取代物理按键的逻辑,不会触发提示
:::
模块间跳转
:::success
一个项目可能会有很多模块,假如A模块要跳转B模块的一个页面,该怎么跳转
:::
:::success
包的分类
- hap- 可以有ability,可以有页面,可以有组件,可以有资源
- hsp- 共享包- 可以实现按需打包
- har- 静态资源包- 可以实现资源共享
.app 上架包
假如是性能优先 建议使用har包
假如是体积优先 建议使用hsp包
:::
:::
:::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修饰的自界说组件命名并导出,'hsp_test’是自界说的名字
- @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四个状态,如下图所示。
Ability创建时回调,执行初始化业务逻辑操纵。
Ability生命周期回调,在烧毁时回调,执行资源清算等操纵。
当WindowStage创建后调用。
当WindowStage烧毁后调用。
Ability生命周期回调,当应用从后台转到前台时触发。
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',
- };
复制代码
- let want: Want = {
- 'deviceId': '', // deviceId为空表示本设备
- 'bundleName': 'com.itheima.studu_case',
- 'abilityName': 'PayAbility',
- };
- (getContext() as common.UIAbilityContext)
- .startAbility(want)
复制代码 :::success
假设我们想调用第三方的包可不可以?
答复: 当然可以,我们只需要知道第三方的包名即可
:::
接下来,我们需要转达参数了
- 我们需要拉起Ability的时候,传已往一个订单id,让付出能够拿到这个id进行付出相关的事宜
- 传参使用parameters,它是一个object范例,你可以转达你想传的任意数据
- 在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
复制代码
- 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>
复制代码
- 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@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})
- .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@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()
- }) } .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的事件参数
:::
:::
:::success
判断逻辑,只要发现x坐标在中线偏左,左边就选中,中线偏右右边选中
:::
- enum SelectType {
- DElETE,
- TEXT,
- NONE
- }
复制代码
- 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@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()
- .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页面,可以正常通过欣赏器访问,我们需要在应用中进行下载解压到我们的沙箱目录下,而且下载完成能够正常访问
:::
- 准备可访问的网络资源(压缩包)
:::success
https://gitee.com/shuiruohanyu/toutiao_net/raw/master/resources/toutiao.zip
:::
- 实现下载方法
- 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
- })
- }
- }
复制代码
- // 解压文件
- async decompressFile () {
- try {
- await zlib.decompressFile(this.filePath, getContext().filesDir)
- router.pushUrl({
- url: 'pages/06/WebCase'
- })
- }catch(error) {
- AlertDialog.show({
- message: error.message
- })
- }
- }
复制代码
- 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@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({
- 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@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 {
- 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的用法根本一致
- 使用方法
- 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%')
- }
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |