134.鸿蒙基础02
沉浸式导航+键盘避让:::success
官方文档(有权限者可观看): https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-develop-apply-immersive-effects-0000001820435461
:::
[*]沉浸式导航
准备一张占满整个屏幕的图片,以我的自拍为例,假如你是在预览器里面打开
https://i-blog.csdnimg.cn/direct/a5524908359b47a0aad2ec4db390bb53.png
这么看没有题目,假如你是在模拟器大概真机打开,你会发现上下两部分被空出来
https://i-blog.csdnimg.cn/direct/349d4636bd74482ea8d0d1abc9ce1158.png
:::info
空出来的这两部分叫做安全区,所谓沉浸式指的得就是关闭内置安全区,自己添补安全区的内容
:::
:::success
怎么实现呢?
两种方案
[*]使用windowStage的设置全屏的方式
[*]使用组件的安全区域扩展的方式
:::
使用windowStage来设置
:::success
window非前端window,鸿蒙中属于窗口管理对象,
:::
:::success
在ability中通过getMainWindow可以获取主窗体,然后通过得到的window对象设置全屏即可实现
:::
windowStage.getMainWindow().then(window => {
window.setWindowLayoutFullScreen(true)
})
https://i-blog.csdnimg.cn/direct/87b85393945b4bc3b3ecc4a975272ab8.pnghttps://i-blog.csdnimg.cn/direct/cb62f5a0e1684e28a73a536d704a355e.png
:::success
通过这种方式最简朴,但是相当于给全部的页面都设置了沉浸式,假如某些页面不需要设置沉浸式,还需要在页面中通过获取window来关闭
:::
https://i-blog.csdnimg.cn/direct/a614d6e877fd4afbab5bdf5ed2bb537f.png
[*]页面中关闭沉浸式
aboutToAppear(): void {
window.getLastWindow(getContext())
.then(win => {
win.setWindowLayoutFullScreen(false)
})
}
https://i-blog.csdnimg.cn/direct/b1d49893c92e4cb1bcfab041cd53be7b.png
:::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}`
})
https://i-blog.csdnimg.cn/direct/aec381898e3246d1a889ff9075bc6f50.png
:::success
由于获取的安全区域的大小是px,以是需要用到vp的话 需要使用pxtovp的方法来实现
:::
[*]开启沉浸式页面自界说安全区颜色
https://i-blog.csdnimg.cn/direct/62ec9aa1b9094157961a384335adf4e4.png
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(, )
键盘避让模式
:::success
当我们存在输入框的页面,假如点击输入框,此时就会弹出键盘,此时键盘的弹出会出题目,如下图
:::
https://i-blog.csdnimg.cn/direct/78509018681d4085b8653ebc8be4df53.png
@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%')
}
}
https://i-blog.csdnimg.cn/direct/ca7a337284d3416ba204e264bbf01499.png
:::success
我们可以设置键盘的避让模式,让窗口被键盘压缩,默认情况下,窗口和键盘的情况是如许的
https://i-blog.csdnimg.cn/direct/acb472172a92420e92c2ff308b4b0b42.png
设置为压缩就酿成
https://i-blog.csdnimg.cn/direct/cffc67b0430244e68d98aadf642fed5d.png
:::
[*]设置方式 - ability中设置
import {KeyboardAvoidMode } from '@kit.ArkUI';
windowStage.getMainWindowSync().getUIContext()
.setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
https://i-blog.csdnimg.cn/direct/8bae10af96c9477b82621321d40e2c3a.png
https://i-blog.csdnimg.cn/direct/87e9c2e52efc4a858ea57488b80d8371.png
路由控制
:::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:https://i-blog.csdnimg.cn/direct/cb326cdca71f4967b6967aa9dab739df.png
B:https://i-blog.csdnimg.cn/direct/0dce9b5b9dc64854a98112dc49c08a6e.png
C:https://i-blog.csdnimg.cn/direct/a5202e50299f459c93c1baa25e52fd01.png
:::success
使用Navigation在不同装备中会得到不同的视觉体验
:::
https://i-blog.csdnimg.cn/direct/df94c5e49a8b41b2b841f4dad0842bb1.png
api11- Navigation-NavPathStack
:::info
用法调整:使用NavPathStack+.navDestination()控制
1.主页内容仍旧写在Navigation中,跳转不再需要NavRouter组件,而是new NavPathStack()后跳转
2.跳转的页面放置.navDestination()中,传入一个自界说构建函数,在函数中条件渲染
总结:new NavPathStack()后的实例可以用于跳转,无论如何都会打开一个页面,渲染的内容仍旧是.navDestination()中条件渲染的内容,假如没有满足的条件,就是一个空缺页
:::
滚动缩小标题
!https://i-blog.csdnimg.cn/direct/0700230287004b5c80a519694426ef52.png
https://i-blog.csdnimg.cn/direct/edbc3ef5815346d490d9d0ce65e972d0.png
隐蔽标题栏
https://i-blog.csdnimg.cn/direct/d5a9b127a7be46ef83cec974b39c26c4.png
自界说返回图标+带参数跳转
!https://i-blog.csdnimg.cn/direct/e4cb55a2fbf14afdbe51af1e87dc356f.png
https://i-blog.csdnimg.cn/direct/c4ecd72a188e45a6a0165b1df908d089.png
不满足条件渲染时表现空缺页面
https://i-blog.csdnimg.cn/direct/8a312c8012d04734a2327fda10974f8c.png
@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,真机不会出现这个题目)
https://i-blog.csdnimg.cn/direct/5e881c2dd7d2402b98c9a52e2b8ab0cf.png
:::
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()
https://i-blog.csdnimg.cn/direct/2010b3a88fc24f7faf9a388a09b99d1e.png
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
:::
https://i-blog.csdnimg.cn/direct/97ee035f26734f9996d284d9a867fe7b.png
router.pushUrl({
url: 'pages/03/RouterCase'
}, router.RouterMode.Single)
[*]showAlertBeforeBackPage
router.showAlertBeforeBackPage({
message: '确定要退出吗'
})
https://i-blog.csdnimg.cn/direct/33fede6b0f3946b19f8ce4ced407b768.png
:::success
该方法只需要在返回之前执行一下即可,建议进入页面前执行
[*]不能获取点击了确定还是取消,由它本身进行处理
[*]不论物理按键还是back都会触发
[*]假如有onBackPress会被取代物理按键的逻辑,不会触发提示
:::
模块间跳转
:::success
一个项目可能会有很多模块,假如A模块要跳转B模块的一个页面,该怎么跳转
:::
:::success
包的分类
[*]hap- 可以有ability,可以有页面,可以有组件,可以有资源
[*]hsp- 共享包- 可以实现按需打包
[*]har- 静态资源包- 可以实现资源共享
.app 上架包
假如是性能优先 建议使用har包
假如是体积优先 建议使用hsp包
:::
[*]新建一个共享包
:::success
阐明
https://i-blog.csdnimg.cn/direct/8aa894a7c3a34dfd8818f46a08ba5c0c.png
:::
:::success
har包不可新建page页面,
hap包同entry一样,此时我们要建一个share共享包即最终会生成hsp
:::
https://i-blog.csdnimg.cn/direct/e365d420ec9a42a6816d8abf11bb52e2.png
使用地点跳转
https://i-blog.csdnimg.cn/direct/e39dfafb5c2644d09631bbe411399bc1.png
router.pushUrl({
url: '@bundle:com.itheima.studu_case/library/ets/pages/Index'
})
:::success
@bundle:包名/模块名/ets/pages/xxx
跳转方式
注意:
此时需要使用模拟器,而且需要摆设你要跳转的hsp包才可以
https://i-blog.csdnimg.cn/direct/9775eb5c03324d61b168d27dd593ae1a.png
https://i-blog.csdnimg.cn/direct/e5c7090619e143e385bd94e1c1e1477d.png
:::
使用路径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组件,以是页面组件同时拥有自界说组件的生命周期
https://i-blog.csdnimg.cn/direct/fb09938431574d949f846086db4420f6.png
在hmlist中测试一下
aboutToAppear() {
console.log("页面初始化")
}
onPageShow() {
console.log("页面显示")
}
onPageHide() {
console.log("页面隐藏")
}
aboutToDisAppear() {
// 清理定时器
console.log("页面销毁")
}
onBackPress() {
console.log("后退键")
}
:::success
更多的逻辑会在aboutToAppear中做数据加载
onPageShow也可以做数据加载 分场景
生存场景下- 送菜送外卖的网约车 具偶然效性的业务 需要在onPageShow
偏固定性场景获取一次数据就行
aboutToDisAppear
清算定时任务 。清算监听-线程监听-进程监听
:::
https://i-blog.csdnimg.cn/direct/d3c061c747924dd6b538930560a17320.png
:::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四个状态,如下图所示。
https://i-blog.csdnimg.cn/direct/d154623409f647ef9eebb73407e531f7.png
[*]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
https://i-blog.csdnimg.cn/direct/9bc2faa37ca248f482adf16aa7ce0b1f.png
https://i-blog.csdnimg.cn/direct/5522b04b21b446f8b0e864ce5fa48f49.png
[*]新建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)
}
}
https://i-blog.csdnimg.cn/direct/4566b21a39444b1b8a34913692105a78.png
[*]新建一个主页Page- MainPage用来跳转到付出Ability
@Entry
@Component
struct MainPage {
build() {
Row() {
Column({ space: 15 }){
Text("主Ability")
.fontSize(50)
Button("去支付")
.width('100%')
}
}
.height('100%')
.padding(20)
}
}
https://i-blog.csdnimg.cn/direct/f79c0dddde6543068b17f8c91bb662f1.png
:::info
[*]ability的拉起必须通过模拟器-以是把我们主Ability的启动页设置为我们刚刚新建的主页
https://i-blog.csdnimg.cn/direct/8be4967e0902459489ea1e63be16a0bf.png
:::
:::info
接下来,我们点击去付出按钮的时候 要拉起付出PayAbility
我们采用当前Ability的上下文来实现,使用文档链接
https://i-blog.csdnimg.cn/direct/8900ac1b1a0d4f1f98875367447ea43c.png
:::
[*]使用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传参数
https://i-blog.csdnimg.cn/direct/347e7a5dc8174ce88eb34d8f3bef658a.png
[*]在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
https://i-blog.csdnimg.cn/direct/2a714a865fc74e309392bef370bad853.png
[*]执行完副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)
})
https://i-blog.csdnimg.cn/direct/4b74ba6f95804a4ba5f2cb0bd4c6c975.png
:::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: '付出失败' }) } https://i-blog.csdnimg.cn/direct/0d8a6303951341479a6965949618da12.png
https://i-blog.csdnimg.cn/direct/a6b2fc16c3dd4aaf9c2b1df1113e5658.png
使用动画
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%')
}
}
https://i-blog.csdnimg.cn/direct/388407d4bf3d43eea209b4e83c32c851.png
[*]通过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%')
}
}
https://i-blog.csdnimg.cn/direct/f66938e46bfb4bd09cf138db1bae79c8.png
[*]通过@animator
:::info
之前两种方式都使用于单次执行动画,假如有一个动画需要重复执行,而且还需要开关控制,这种复杂的动画,更适合交给animator类来实现,我们实现一个播放状态CD旋转,暂停状态CD制止旋转的效果
:::
https://i-blog.csdnimg.cn/direct/28ba22718e304e39a65fa7702594ec5c.png
:::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
训练:做一个心跳的案例吧,使用之前的点赞图标
:::
https://i-blog.csdnimg.cn/direct/8e567795ae954e6d881c67ef3ff60d99.png
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%')
}
}
https://i-blog.csdnimg.cn/direct/5bf4c18349c14ec589dc19daea82e8b0.png
:::success
通过state属性可以控制图片的动画的执行方式
AnimationStatus.Initial 初始化 - 不播放动画
AnimationStatus.Running 播放中 - 播放动画
AnimationStatus.Paused 暂停 - 暂停动画至当前帧
生成一个长度为10的数组:
Array(10)
设置数组每一项的内容:
Array.form(Array(10),(item,index)=>{
return ${index}
})
:::
3.转场动画
:::success
[*] **共享元素转场 **
[*] 出现/消失转场
[*] 模态转场 bindSheet 半模态/bindContentCover 全模态
[*] 组件内转场 transition属性
[*] 页面专场(不保举)
:::
[*] 共享元素转场
https://i-blog.csdnimg.cn/direct/484651f5775b4c6ba6aab11edf716832.png
:::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
模态转场是新的界面覆盖在旧的界面上,旧的界面不消失的一种转场方式。
:::
https://i-blog.csdnimg.cn/direct/26c0bac9a7c54c37936a2de0491bb21d.png
:::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
}
})
}
}
https://i-blog.csdnimg.cn/direct/7a37eca81861457098f9a85705878f21.png
:::success
全模态和半模态弹层使用方式一样,第三个参数可以设置弹层的 modalTransition 表现模式
:::
[*]组件内元素专场transition
:::success
组件内转场主要通过transition属性配置转场参数,在组件插入和删除时表现过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。
:::
https://i-blog.csdnimg.cn/direct/2ef2efc4fa054870b2dff1688b9e40a3.png
:::success
4.0中的我们使用的transitionOption的属性被废弃了,新增了TransitionEffect的属性设置方式
:::
https://i-blog.csdnimg.cn/direct/304848249f56465c840269d4a57361e3.png
https://i-blog.csdnimg.cn/direct/90c35b0c34d54aff839861b685d39aec.png
https://i-blog.csdnimg.cn/direct/0c805ec702ae4f21ba6620c9e22c6cf6.png
https://i-blog.csdnimg.cn/direct/3add206bbf0a49fbaaa17ab5a0b2a4a2.png
:::success
语法
.transition(TransitionEffect.SLIDE.animation({
duration: 1000
}).combine(TransitionEffect.rotate({
angle: -180
})).combine(TransitionEffect.translate({
x: ‘-100%’
})))
有三种模式可选
https://i-blog.csdnimg.cn/direct/2beb813e9c1641ea82a7e52b26aa76e5.png
:::
TransitionEffect.translate({x:'-100%'}).animation({duration:2000})
.combine(TransitionEffect.rotate({angle:360}).animation({duration:1000}))
@Entry@Componentstruct ComAnCase {@StateshowImage: boolean = falsebuild() { 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%')}} https://i-blog.csdnimg.cn/direct/5715749a1f2843c9ac490d7436c55833.png
:::success
依赖于一个模式才气触发,不如自界说动画灵活,相识即可
:::
[*]页面转场动画(不保举)
https://i-blog.csdnimg.cn/direct/d7084a9813ba4a1eadc88a1a1f59ffe4.png
https://i-blog.csdnimg.cn/direct/31c01e54bee3496d9428172400fc70ca.png
使用方法为:
声明转场动画,包含入场和离场两个函数,进行样式的控制即可
https://i-blog.csdnimg.cn/direct/fbd79386322c4422920cbc27ddc86fa0.png
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
为组件绑定不同范例的手势事件,并设置事件的响应方法。
:::
https://i-blog.csdnimg.cn/direct/4b61f569d6964e4e9baea8db303f7436.png
:::success
一样寻常情况下 使用组件的gesture即可
:::
[*]手势范例
https://i-blog.csdnimg.cn/direct/d7d402041a3a47dc93167406f2b1d8c1.png
:::success
我们这里学习两个,长按手势宁静移手势
语法
.gesture( LongPressGesture().onAction(() => {}) )
:::
[*]长按手势LongPressGesture
:::success
https://i-blog.csdnimg.cn/direct/1361e555e0374d3d8b54c9d11e66f0b4.png
:::
:::success
根本上全部的手势都会有这三个事件
:::
[*]实现一个功能-长按语音按钮,表现语音录制框
https://i-blog.csdnimg.cn/direct/51c4465284764f6b9ddf8115068f84cd.png
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
https://i-blog.csdnimg.cn/direct/cafbc8dee8284501ae25467e9b632a99.png
:::
结合原来的长按,长按基础上,拖动实现删除大概文本按钮的选中
:::success
此时需要使用组合手势,由于是长按和拖动手势的集合
组合手势GestureGroup(mode: GestureMode, …gesture: GestureType[])
GestureMode
https://i-blog.csdnimg.cn/direct/e9afb7442f4d459397f56ba97dc3baa9.png
GestureEvent的事件参数
https://i-blog.csdnimg.cn/direct/fd34dd8ccba84dec90960ae35ad2602f.png
https://i-blog.csdnimg.cn/direct/ee8c96688f9a4585b8387db2c79c75d7.png
https://i-blog.csdnimg.cn/direct/f3666e1bac4546ff9b157ed549fa332e.png
:::
[*]手指的坐标信息
:::success
https://i-blog.csdnimg.cn/direct/5b3e9cfff10e4bbabd8d04e64beb6e39.png
:::
:::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.globalX < this.screenWidth / 2) {
this.currentMode = SelectType.DElETE
}else {
this.currentMode = SelectType.TEXT
}
})
.onActionEnd(() => {
this.currentMode = SelectType.NONE
})
https://i-blog.csdnimg.cn/direct/9fc1fe62faeb43bdbea31dc6a230aade.png
:::success
完成代码
:::
import { util } from '@kit.ArkTS'import {deviceInfo } from '@kit.BasicServicesKit'import { display, promptAction } from '@kit.ArkUI'@Entry@Componentstruct GestureCase {@StateshowVoice: boolean = falsescreenWidth: number = 0@StatecurrentMode: SelectType = SelectType.NONE@BuildergetContent() { 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.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同时颠末用户的相应授权才气进行。
:::
https://i-blog.csdnimg.cn/direct/33e82f86106c433686698dd2b86eaf79.png
:::success
应用文件目录与应用文件路径
如前文所述,“应用沙箱目录”内分为两类:应用文件目录和体系文件目录。
体系文件目录对应用的可见范围由体系预置,开发者无需关注。
在此主要介绍应用文件目录,如下图所示。应用文件目录下某个文件或某个具体目录的路径称为应用文件路径。应用文件目录下的各个文件路径,具备不同的属性和特征。
图3 应用文件目录结构图
:::
https://i-blog.csdnimg.cn/direct/58010cfad19a4ccfb21ace45e3155cbe.png
[*]获取沙箱目录
:::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%')
}
}
https://i-blog.csdnimg.cn/direct/230621d278144f879dcaed3943c95dbb.png
:::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
})
}
}
https://i-blog.csdnimg.cn/direct/9f08436e49874c8da8eb3481c0e34b5a.png
[*]解压zip包
:::success
使用zlib模块
https://i-blog.csdnimg.cn/direct/83ac34e796214c8491894d7ec8c48281.png
:::
// 解压文件
async decompressFile () {
try {
await zlib.decompressFile(this.filePath, getContext().filesDir)
}catch(error) {
AlertDialog.show({
message: error.message
})
}
}
https://i-blog.csdnimg.cn/direct/a3c31a825994455ab7337f78d7715dd4.png
[*]解压后跳转到拥有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里面的内容就不被允许执行了,会报错
:::
https://i-blog.csdnimg.cn/direct/55b66056207b4d39ae470ee4fec28a54.png
原生本领
1. 音视频播放
[*]视频播放
https://i-blog.csdnimg.cn/direct/5b084df26fc647d39457d4a74257da30.png
arkUI提供了Video组件可以直接播放视频,并提供自带的播放-暂停 全屏,拖动进度等功能
用法
[*]Video提供构造参数 Video({ src: string | Resource })
[*]src支持在线路径-和本地资源路径
[*]示例
Video({
src:'https://video19.ifeng.com/video09/2024/05/23/p7199260608686989961-0-122707.mp4'
})
版权阐明: 上述代码中的视频链接为参考学习,并非用作贸易用途,请同学们自行放置的外链视频链接
https://i-blog.csdnimg.cn/direct/2b029f3e2a424095bb8636fc13fcd293.png
[*]放映本地视频
本地视频我们需要放置在资源目录的原始文件中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 {@Statespeed: number = 1build() { 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控制视频 暂停- 播放-制止-全屏-静音-播放速度- 播放进度
https://i-blog.csdnimg.cn/direct/875c2cccbef84e7690a5f0a16652dec7.png
自界说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. 抖音小案例
https://i-blog.csdnimg.cn/direct/7673e01d1af645ffa6765565149d0247.png
[*]声明范例和数据
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 {@Statelist: VideoItem[] = allData@StateactiveIndex: number = 0build() { 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: numberchangeVideo() { this.activeIndex === this.index ? this.controller.start() : this.controller.pause()}controller: VideoController = new VideoController()@StateisPlay:boolean = truebuild() { 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. 绘画本领-画布组件
https://i-blog.csdnimg.cn/direct/5440ad8d22094e7f952084464fb488d7.png
[*]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%')
}
}
绘制其他内容
https://i-blog.csdnimg.cn/direct/918e5fdffab54eb4bc0732b0830e4eee.png
@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 = '100pxsans-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小球,提供代码,感爱好的同学自己模拟创造吧,复杂的动画往往需要复杂的算法和物理知识,比如向心力、能量守恒、摩擦力、加快度、三角函数等等
:::
[*]地球公转
https://i-blog.csdnimg.cn/direct/355105c8cb83472d8c852586b842e813.png
素材:素材.zip
太阳 https://i-blog.csdnimg.cn/direct/d8c9cd3151fb40168aaa3d977dc36c6f.png
地球 https://i-blog.csdnimg.cn/direct/f88dbfe717074fbb9626f4181df800d3.png
月球 https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2024%2Fpng%2F34522383%2F1716728141676-8ee42540-a2be-4dc8-80ed-7d4c35db5087.png%23averageHue%3D%2523a6a6a6%26clientId%3Du8ecf1ce2-2515-4%26from%3Dpaste%26height%3D6%26id%3Du609d8a1e%26originHeight%3D7%26originWidth%3D7%26originalType%3Dbinary%26ratio%3D1.100000023841858%26rotation%3D0%26showTitle%3Dfalse%26size%3D278%26status%3Ddone%26style%3Dnone%26taskId%3Duf54366d6-5cbf-4b18-86bc-b727661b4ce%26title%3D%26width%3D6.363636225708263&pos_id=img-tMe6BT1h-1730184610951
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)
}
[*]呆板猫
https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcdn.nlark.com%2Fyuque%2F0%2F2024%2Fpng%2F34522383%2F1716720570901-f12d7f9d-e995-4774-9d42-e599af5a61eb.png%23averageHue%3D%2523e1e1e1%26clientId%3Du38842701-69ad-4%26from%3Dpaste%26height%3D414%26id%3Dufcb313d8%26originHeight%3D455%26originWidth%3D505%26originalType%3Dbinary%26ratio%3D1.100000023841858%26rotation%3D0%26showTitle%3Dfalse%26size%3D47083%26status%3Ddone%26style%3Dnone%26taskId%3Du0d5a015c-a40e-447f-a954-21d9fb1833a%26title%3D%26width%3D459.09089914038185&pos_id=img-jAKc2fvh-1730184610952
drawRobotCat() {
this.myPen.lineWidth = 2
const center =
const r = 240
//1.以画布中心为圆心,画出头
this.myPen.arc(center, center, 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+offset,center+offset);//130,420
this.myPen.bezierCurveTo(92, 147, 0, 322, center - offset, center + offset)
this.myPen.moveTo(398, 138)
this.myPen.bezierCurveTo(576, 138, 550, 402, center + offset, center + 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 - offset, center + offset)
this.myPen.bezierCurveTo(208, 505, 348, 496, center + offset, center + offset)
this.myPen.moveTo(center - offset, center + offset)
this.myPen.bezierCurveTo(center - offset - 10, center + offset + 10, center - offset, center + offset + 20, center - offset, center + offset + 20)
this.myPen.moveTo(center + offset, center + offset)
this.myPen.bezierCurveTo(center + offset + 10, center + offset + 10, center + offset, center + offset + 20, center + offset, center + offset + 20)
this.myPen.moveTo(center - offset, center + offset + 20)
this.myPen.bezierCurveTo(208, 525, 348, 516, center + offset, center + offset + 20)
this.myPen.stroke()
}
3D小球:
https://i-blog.csdnimg.cn/direct/8fc6e050b53149f986d82194c8912f2a.png
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.签字版
https://i-blog.csdnimg.cn/direct/3bfd8e72019a47c781a2c2c6c8e2e9be.png
[*]接下来需要处理什么时候开始在画板上画的机遇题目了
[*]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.x, event.touches.y)
} else if (event.type === TouchType.Move) {
this.myPen.lineTo(event.touches.x, event.touches.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
https://i-blog.csdnimg.cn/direct/ba9d0546785e4d139d59093523d8fa91.png
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.x, event.touches.y)
} else if (event.type === TouchType.Move) {
this.myPen.lineTo(event.touches.x, event.touches.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企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]