面试录音

应用权限
应用权限概述
系统提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用。
应用申请敏感权限时,必须填写权限使用理由字段,敏感权限通常是指与用户隐私密切相关的权限,包括地理位置、相机、麦克风、日历、健身运动、身体传感器、音乐、文件、图片视频等权限。参考向用户申请授权。
- system_grant
在配置文件中,声明应用需要请求的权限后,系统会在安装应用时自动为其进行权限预授予,开发者不需要做其他操作即可使用权限。
- user_grant
- 在配置文件中,声明应用需要请求的权限,且要设置需要使用的场景+使用原因
- 调用 requestPermissionsFromUser() 方法后,应用程序将等待用户授权的结果。如果用户授权,则可以继续访问目标操作。如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。可参考二次向用户申请权限 requestPermissionOnSetting() 。
module.json5
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{
"name": "ohos.permission.MICROPHONE",
"usedScene": {},
"reason": "$string:reason_microphone"
}
],
原因格式:用于xxx模块xxx功能
permission 工具
目标:封装权限工具,提供请求用户权限,拉起用户权限设置的能力
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
class Permission {
// 请求用户授权
async requestPermissions(permissions: Permissions[]) {
const atManager = abilityAccessCtrl.createAtManager()
const ctx = AppStorage.get<Context>('context')
if (ctx) {
const result = await atManager.requestPermissionsFromUser(ctx, permissions)
return result.authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
}
return false
}
// 打开权限设置 beta3
async openPermissionSetting(permissions: Permissions[]) {
const atManager = abilityAccessCtrl.createAtManager()
const ctx = AppStorage.get<Context>('context')
if (ctx) {
const authResults = await atManager.requestPermissionOnSetting(ctx, permissions)
return authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
}
return false
}
}
export const permission = new Permission()
录音授权
目标:使用权限请求工具,在录音页面实现请求权限,无权限不可进入



permissions: Permissions[] = ['ohos.permission.MICROPHONE']
confirmConfig: promptAction.ShowDialogOptions = {
title: "温馨提示",
message: "未授权使用麦克风将无法使用该面试录音功能,是否前往设置进行授权?",
buttons: [
{ text: '离开', color: $r('app.color.common_gray_01') },
{ text: '去授权', color: $r('app.color.black') }
]
}
async getPermission () {
try {
// 第一请求授权
const isOk = await permission.requestPermissions(this.permissions)
if (isOk) return
// 弹窗提示
const confirm = await promptAction.showDialog(this.confirmConfig)
if (confirm.index === 1) {
const isOk2 = await permission.openPermissionSetting(this.permissions)
if (isOk2) return
}
router.back()
} catch (e) {
promptAction.showToast({ message: '未授权' })
router.back()
}
}
aboutToAppear(): void {
this.getPermission()
}
录音知识
使用 AvRecorder 录音
目标:使用 AvRecorder 实现音频录制存储到应用沙箱
实现步骤:
- 需要一个文件接收音频数据
- 准备录音配置
- 使用 AvRecorder 实现开始录音,结束录音
落地代码:
avRecorder?: media.AVRecorder
fd?: number
filePath?: string
async startRecord() {
// 1. 准备一个文件接收录音
const ctx = getContext(this)
const filePath = ctx.filesDir + '/' + Date.now() + '.m4a'
this.filePath = filePath
const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
this.fd = file.fd
// 2. 准备路由配置对象
const config: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
profile: {
audioBitrate: 100000, // 音频比特率
audioChannels: 1, // 音频声道数
audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
audioSampleRate: 48000, // 音频采样率
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
},
url: `fd://${file.fd}`
}
// 3. 开始录制
const avRecorder = await media.createAVRecorder()
await avRecorder.prepare(config)
await avRecorder.start()
this.avRecorder = avRecorder
}
async stopRecord() {
if (this.avRecorder) {
await this.avRecorder.stop()
await this.avRecorder.release()
fileIo.closeSync(this.fd)
}
}
Button('开始录音')
.onClick(() => {
this.startRecord()
})
Button('结束录音')
.onClick(() => {
this.stopRecord()
})
录音声音振动效果
目标:根据声音的大小实现声音振动特效
实现步骤:
- 通过 getAudioCapturerMaxAmplitude 观察音频区间
- 封装振动组件,通过声音振幅数据实现振动效果
落地代码:
1)获取振幅数据,出入振动组件 AudioPage.ets
timer?: number
@State maxAmplitude: number = 0
// startRecord 4. 每100ms获取一下声音振幅
this.timer = setInterval(async () => {
this.maxAmplitude = await avRecorder.getAudioCapturerMaxAmplitude()
logger.debug('startRecord', this.maxAmplitude.toString())
}, 100)
// stopRecord 清理定时器
clearInterval(this.timer)
AudioBoComp({ maxAmplitude: this.maxAmplitude })
2)实现振动组件 Audio/AudioBoComp.ets
@Component
struct AudioBoComp {
@Prop @Watch('onChange') maxAmplitude: number
@State per: number = 0
onChange() {
animateTo({ duration: 100 }, () => {
if (this.maxAmplitude < 500) {
this.per = 0
} else if (this.maxAmplitude > 30000) {
this.per = 1
} else {
this.per = this.maxAmplitude / 30000
}
})
}
build() {
Row({ space: 5 }) {
ForEach(Array.from({ length: 30 }), () => {
Column()
.layoutWeight(1)
.height(this.per * 100 * Math.random())
.backgroundColor($r('app.color.common_blue'))
})
}
.width('100%')
.height(100)
}
}
使用 AvPlayer 播放
目标:能够使用 AvPlayer 播放应用沙箱中的音频文件,且显示进度条
落地代码:
avPlayer?: media.AVPlayer
@State total: number = 0
@State value: number = 0
async startPlay() {
try {
const file = fileIo.openSync(this.filePath, fileIo.OpenMode.READ_ONLY)
const avPlayer = await media.createAVPlayer()
avPlayer.on('stateChange', state => {
if (state === 'initialized') {
avPlayer.prepare()
} else if ( state === 'prepared') {
avPlayer.loop = true
this.total = avPlayer.duration
avPlayer.play()
}
})
// 当前播放时间改变
avPlayer.on('timeUpdate', (time) => {
this.value = time
})
avPlayer.url = `fd://${file.fd}`
this.avPlayer = avPlayer
} catch (e) {
logger.error('startPlay', JSON.stringify(e))
}
}
stopPlay() {
if (this.avPlayer) {
this.avPlayer.stop()
this.avPlayer.release()
}
}
Button('开始播放')
.onClick(() => {
this.startPlay()
})
Button('停止播放')
.onClick(() => {
this.stopPlay()
})
Progress({ total: this.total, value: this.value })
.width('100%')
关系型数据库知识
数据库概述
关系型数据库(Relational Database,RDB)是一种基于关系模型来管理数据的数据库。关系型数据库基于SQLite组件提供了一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。不支持Worker线程。
ArkTS侧支持的基本数据类型:number、string、二进制类型数据、boolean。为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。
该模块提供以下关系型数据库相关的常用功能:
- RdbStore:提供管理关系数据库(RDB)方法的接口。
- RdbPredicates:数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数据库的操作条件。
- ResultSet:提供用户调用关系型数据库查询接口之后返回的结果集合。
创建数据库
CREATE TABLE IF NOT EXISTS article (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
create_time INTEGER NOT NULL
)
创建一个文章数据库:
store?: relationalStore.RdbStore
tableName = 'article'
async createStore () {
const store = await relationalStore.getRdbStore(getContext(this), {
name: 'interview_tong.db',
securityLevel: relationalStore.SecurityLevel.S1
})
store.executeSql(`
CREATE TABLE IF NOT EXISTS ${this.tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
create_time INTEGER NOT NULL
)
`)
this.store = store
}
aboutToAppear(): void {
this.createStore()
}
插入数据
Button('添加')
.onClick(() => {
this.store?.insert(this.tableName, {
id: null,
title: '测试' + Math.random(),
content: '我是一篇测试文章' + Math.random(),
create_time: Date.now()
})
})
查询数据
Button('查询总条数')
.onClick(async () => {
const predicates = new relationalStore.RdbPredicates(this.tableName)
const resultSet = await this.store?.query(predicates)
this.total = resultSet?.rowCount || 0
})
Text('总条数' + this.total)
Button('查询所有数据')
.onClick(async () => {
const predicates = new relationalStore.RdbPredicates(this.tableName)
const resultSet = await this.store?.query(predicates)
const list: ArticleItem[] = []
while (resultSet?.goToNextRow()) {
list.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
})
}
resultSet?.close()
this.list = list
})
Text(JSON.stringify(this.list))
修改数据
Button('修改第一条')
.onClick(() => {
const item = this.list[0]
item.title = '修改标题' + Math.random()
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('id', item.id)
this.store?.updateSync(item, predicates)
})
删除数据
Button('删除第一条')
.onClick(() => {
const item = this.list[0]
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('id', item.id)
this.store?.deleteSync(predicates)
})
删除数据库
deleteStore () {
relationalStore.deleteRdbStore(getContext(this), {
name: 'interview.db',
securityLevel: relationalStore.SecurityLevel.S1
})
}
Button('删除数据库')
.onClick(() => {
this.deleteStore()
})
audioDB工具-创建数据库
目标:封装一个操作录音数据库的工具,提供创建数据库的方法
实现步骤:
- 约定好数据库的表结构
- 封装工具类,提供一个创建数据库的方法
落地代码:
1)表结构
CREATE TABLE IF NOT EXISTS interview_audio (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
duration INTEGER NOT NULL,
size INTEGER NOT NULL
)
2)封装类
import { relationalStore } from '@kit.ArkData'
class AudioDB {
store?: relationalStore.RdbStore
tableName = 'interview_audio'
// 初始化数据库
async initStore() {
const ctx = AppStorage.get<Context>('context')
if (ctx) {
const store = await relationalStore.getRdbStore(ctx, {
name: 'interview_audio.db',
securityLevel: relationalStore.SecurityLevel.S1
})
const sql = `
CREATE TABLE IF NOT EXISTS ${this.tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
duration INTEGER NOT NULL,
size INTEGER NOT NULL
)
`
await store.executeSql(sql)
this.store = store
}
}
}
audioDB工具-数据操作方法
目标:提供 添加 删除 查询 修改 数据库的方法
import { relationalStore, ValuesBucket } from '@kit.ArkData'
export interface InterviewAudioItem extends ValuesBucket {
id: number | null
user_id: string
name: string
path: string
duration: number
size: number
create_time: number
}
class AudioDB {
store?: relationalStore.RdbStore
tableName = 'interview_audio'
// 初始化数据库
async initStore() {
const ctx = AppStorage.get<Context>('context')
if (ctx) {
const store = await relationalStore.getRdbStore(ctx, {
name: 'interview_audio.db',
securityLevel: relationalStore.SecurityLevel.S1
})
const sql = `
CREATE TABLE IF NOT EXISTS ${this.tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
duration INTEGER NOT NULL,
size INTEGER NOT NULL,
create_time INTEGER NOT NULL
)
`
await store.executeSql(sql)
this.store = store
}
}
// 添加
async insert(item: InterviewAudioItem) {
const rowId = await this.store?.insert(this.tableName, item)
if (rowId === undefined || rowId === -1) {
return Promise.reject('insert fail')
} else {
return Promise.resolve()
}
}
// 删除
async delete(id: number) {
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('id', id)
const rowCount = await this.store?.delete(predicates)
if (rowCount === undefined || rowCount <= 0) {
return Promise.reject('delete fail')
} else {
return Promise.resolve()
}
}
// 修改
async update(item: InterviewAudioItem) {
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('id', item.id)
const rowCount = await this.store?.update(item, predicates)
if (rowCount === undefined || rowCount <= 0) {
return Promise.reject('update fail')
} else {
return Promise.resolve()
}
}
// 查询用户
async query(userId: string) {
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('user_id', userId)
const resultSet = await this.store?.query(predicates)
if (!resultSet) {
return Promise.reject('query fail')
}
const list: InterviewAudioItem[] = []
while (resultSet.goToNextRow()) {
list.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
user_id: resultSet.getString(resultSet.getColumnIndex('user_id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
path: resultSet.getString(resultSet.getColumnIndex('path')),
duration: resultSet.getLong(resultSet.getColumnIndex('duration')),
size: resultSet.getLong(resultSet.getColumnIndex('size')),
create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
})
}
resultSet.close()
return Promise.resolve(list)
}
}
export const audioDB = new AudioDB()
面试录音
页面结构
目的:准备页面的组件结构,搭建页面基本效果
pages/AudioPage.ets
import { permission } from '../commons/utils'
import { promptAction, router } from '@kit.ArkUI'
import { Permissions } from '@kit.AbilityKit'
import { AudioView } from '../views/Audio/AudioView'
@Entry
@Component
struct AudioPage {
permissions: Permissions[] = ['ohos.permission.MICROPHONE']
confirmConfig: promptAction.ShowDialogOptions = {
title: "温馨提示",
message: "未授权使用麦克风将无法使用该面试录音功能,是否前往设置进行授权?",
buttons: [
{ text: '离开', color: $r('app.color.common_gray_01') },
{ text: '去授权', color: $r('app.color.black') }
]
}
async getPermission() {
try {
// 第一请求授权
const isOk = await permission.requestPermissions(this.permissions)
if (isOk) {
return
}
// 未授权弹窗提示
const confirm = await promptAction.showDialog(this.confirmConfig)
if (confirm.index === 1) {
// 第二次请求权限
const isOk2 = await permission.openPermissionSetting(this.permissions)
if (isOk2) {
return
}
}
router.back()
} catch (e) {
promptAction.showToast({ message: '未授权' })
router.back()
}
}
build() {
Column() {
AudioView()
}
}
}
views/AudioView.ets
录音视图
import { HcNavBar } from '../../commons/components/HcNavBar'
import { InterviewAudioItem } from '../../commons/utils'
import { AudioItemComp } from './AudioItemComp'
import { AudioRecordComp } from './AudioRecordComp'
@Component
export struct AudioView {
@State list: InterviewAudioItem[] = [{} as InterviewAudioItem, {} as InterviewAudioItem ]
build() {
Column() {
HcNavBar({ title: '面试录音', showRightIcon: false })
Column() {
List() {
ForEach(this.list, (item: InterviewAudioItem) => {
ListItem() {
AudioItemComp({
item: {
id: 1,
name: '2024年10月01日_10点10分10秒',
path: '/data/el/xxx',
user_id: 100,
duration: 10000,
size: 10000,
create_time: 10000
}
})
}
})
}
.width('100%')
.height('100%')
}
.width('100%')
.layoutWeight(1)
AudioRecordComp()
}
.width('100%')
.height('100%')
}
}
views/AudioItemComp.ets
单条录音数据数组
import { InterviewAudioItem } from '../../commons/utils'
@Component
export struct AudioItemComp {
@Prop
item: InterviewAudioItem = {} as InterviewAudioItem
build() {
Row({ space: 15 }) {
Image($r('app.media.ic_mine_audio'))
.width(50)
.aspectRatio(1)
Column({ space: 10 }) {
Text(this.item.name)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 20 }) {
Text(`时长:${(this.item.duration / 1000).toFixed(0)} 秒`)
.fontSize(14)
.fontColor($r('app.color.common_gray_03'))
Text(`大小:${(this.item.size / 1000).toFixed(0)} KB`)
.fontSize(14)
.fontColor($r('app.color.common_gray_03'))
}
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.alignSelf(ItemAlign.Start)
}
.padding(15)
.height(80)
.width('100%')
}
}
views/AudioRecordComp.ets
录音组件
import { media } from '@kit.MediaKit'
import { fileIo } from '@kit.CoreFileKit'
@Component
export struct AudioRecordComp {
@StorageProp('bottomHeight') bottomHeight: number = 0
avRecorder?: media.AVRecorder
fd?: number
filePath?: string
timer?: number
@State maxAmplitude: number = 0
async startRecord() {
// 1. 准备一个文件接收录音
const ctx = getContext(this)
const filePath = ctx.filesDir + '/' + Date.now() + '.m4a'
this.filePath = filePath
const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
this.fd = file.fd
// 2. 准备路由配置对象
const config: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
profile: {
audioBitrate: 100000, // 音频比特率
audioChannels: 1, // 音频声道数
audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
audioSampleRate: 48000, // 音频采样率
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
},
url: `fd://${file.fd}`
}
// 3. 开始录制
const avRecorder = await media.createAVRecorder()
await avRecorder.prepare(config)
await avRecorder.start()
this.avRecorder = avRecorder
// 4. 每100ms获取一下声音振幅
this.timer = setInterval(async () => {
this.maxAmplitude = await avRecorder.getAudioCapturerMaxAmplitude()
}, 100)
}
async stopRecord() {
if (this.avRecorder) {
clearInterval(this.timer)
await this.avRecorder.stop()
await this.avRecorder.release()
fileIo.closeSync(this.fd)
this.maxAmplitude = 0
}
}
build() {
Column() {
AudioBoComp({ maxAmplitude: this.maxAmplitude })
Row() {
Image($r('sys.media.ohos_ic_public_voice'))
.width(24)
.aspectRatio(1)
.fillColor($r('app.color.white'))
.onClick(async () => {
// TODO 开始和停止录音
})
}
.justifyContent(FlexAlign.Center)
.height(50)
.width(50)
.borderRadius(25)
.margin({ top: 20 })
.backgroundColor($r('app.color.black'))
}
.width('100%')
.height(240)
.backgroundColor($r('app.color.common_gray_bg'))
.padding({ bottom: this.bottomHeight, left: 80, right: 80, top: 20 })
}
}
@Component
export struct AudioBoComp {
@Prop @Watch('onChange') maxAmplitude: number
@State per: number = 0
onChange() {
animateTo({ duration: 100 }, () => {
if (this.maxAmplitude < 500) {
this.per = 0
} else if (this.maxAmplitude > 30000) {
this.per = 1
} else {
this.per = this.maxAmplitude / 30000
}
})
}
build() {
Row({ space: 5 }) {
ForEach(Array.from({ length: 30 }), () => {
Column()
.layoutWeight(1)
.height(this.per * 100 * Math.random())
.backgroundColor($r('app.color.common_blue'))
})
}
.width('100%')
.height(100)
.backgroundColor($r('app.color.common_gray_bg'))
}
}
添加录音
目标:点击录音按钮开启录音,再次点击结束录音,存储录音信息
落地代码:
1)组件实现录制状态切换 views/AudioRecordComp.ets
@State recording: boolean = false
Image($r('sys.media.ohos_ic_public_voice'))
.width(24)
.aspectRatio(1)
.fillColor($r('app.color.white'))
.onClick(async () => {
if (this.recording) {
await this.stopRecord()
this.recording = false
// TODO 记录录音
} else {
await this.startRecord()
this.recording = true
}
})
2)组件暴露录制结束事件
onRecordEnd: (item: InterviewAudioItem) => void = () => {}
const stat = fileIo.statSync(this.filePath)
this.onRecordEnd({
id: null,
name: dayjs().format('YYYY年MM月DD日_HH时mm分ss秒'),
path : this.filePath || '',
duration: Date.now() - this.startTime,
size: stat.size,
user_id: auth.getUser().id,
create_time: Date.now()
})
3)父组件在录制结束后,插入数据库完成添加
AudioView.ets
async aboutToAppear() {
await audioDB.initStore()
}
AudioRecordComp({
onRecordEnd: async (item: InterviewAudioItem) => {
await audioDB.insert(item)
// TODO 更新列表
}
})
渲染列表
目标:完成录音列表展示
1)获取数据库录音数据
async getList() {
const user = auth.getUser()
const rows = await audioDB.query(user.id)
this.list = rows
}
async aboutToAppear() {
await audioDB.initStore()
await this.getList()
}
2)渲染列表
ForEach(this.list, (item: InterviewAudioItem) => {
ListItem() {
AudioItemComp({
item: item
})
}
})
删除录音
目标:通过滑动操作完成录音删除
1)准备滑动删除和编辑效果
@Builder
ListItemSwiperBuilder(item: InterviewAudioItem) {
Row() {
Text('编辑')
.actionButton($r('app.color.common_blue'))
Text('删除')
.actionButton('#FF0033')
}
.height('100%')
}
@Extend(Text)
function actionButton(color: ResourceColor) {
.width(80)
.aspectRatio(1)
.backgroundColor(color)
.textAlign(TextAlign.Center)
.fontColor($r('app.color.white'))
}
ListItem() {
AudioItemComp({
item: item
})
}
.swipeAction({
end: this.ListItemSwiperBuilder(item)
})
2)实现删除
Text('删除')
.actionButton('#FF0033')
.onClick(async () => {
await audioDB.delete(item.id!)
this.getList()
})
编辑录音
目标:实现弹窗对话框,修改录音名称
1)准备对话框
@CustomDialog
struct InputDialog {
controller: CustomDialogController
@Prop name: string = ''
onSubmit: (name: string) => void = () => {
}
build() {
Column({ space: 12 }) {
Text('修改名字:')
.height(40)
.fontWeight(500)
TextInput({ text: $$this.name })
Row({ space: 120 }) {
Text('取消')
.fontWeight(500)
.fontColor($r('app.color.common_gray_02'))
.onClick(() => {
this.controller.close()
})
Text('确认')
.fontWeight(500)
.fontColor($r('app.color.common_blue'))
.onClick(() => {
this.onSubmit(this.name)
})
}
.height(40)
.width('100%')
.justifyContent(FlexAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.padding(16)
.borderRadius(12)
.width('80%')
.backgroundColor($r('app.color.white'))
}
}
2)弹出对话框
@State currentItem: InterviewAudioItem = {} as InterviewAudioItem
dialog = new CustomDialogController({
builder: InputDialog({
name: this.currentItem.name,
onSubmit: async (name) => {
// TODO 实现修改
}
}),
customStyle: true,
alignment: DialogAlignment.Center
})
Row() {
Text('编辑')
.actionButton($r('app.color.common_blue'))
.onClick(() => {
this.currentItem = item
this.dialog.open()
})
3)完成修改
dialog = new CustomDialogController({
builder: InputDialog({
name: this.currentItem.name,
onSubmit: async (name) => {
const item = this.currentItem
item.name = name
await audioDB.update(item)
await this.getList()
this.dialog.close()
}
}),
customStyle: true,
alignment: DialogAlignment.Center
})
录音播放
目标:通过全屏模态框实现录音信息展示和播放
1)播放组件准备 views/AudioPlayer.ets
支持播放暂停和进度效果
import { InterviewAudioItem, logger } from '../../commons/utils'
import { media } from '@kit.MediaKit'
import { fileIo } from '@kit.CoreFileKit'
@Component
export struct AudioPlayer {
@State
playing: boolean = false
@Prop item: InterviewAudioItem = {} as InterviewAudioItem
avPlayer?: media.AVPlayer
@State total: number = 0
@State value: number = 0
async startPlay() {
try {
const file = fileIo.openSync(this.item.path, fileIo.OpenMode.READ_ONLY)
const avPlayer = await media.createAVPlayer()
avPlayer.on('stateChange', state => {
if (state === 'initialized') {
avPlayer.prepare()
} else if (state === 'prepared') {
avPlayer.loop = true
this.total = avPlayer.duration
avPlayer.play()
}
})
// 当前播放时间改变
avPlayer.on('timeUpdate', (time) => {
this.value = time
})
avPlayer.url = `fd://${file.fd}`
this.avPlayer = avPlayer
this.playing = true
} catch (e) {
logger.error('startPlay', JSON.stringify(e))
}
}
stopPlay() {
if (this.avPlayer) {
this.avPlayer.stop()
this.avPlayer.release()
this.playing = false
}
}
aboutToAppear(): void {
if (this.playing) {
this.stopPlay()
}
}
build() {
Column({ space: 20 }) {
Image($r('app.media.ic_mine_audio'))
.width(100)
.aspectRatio(1)
Text(this.item.name)
.fontSize(18)
Row({ space: 20 }) {
Image(!this.playing ? $r('sys.media.ohos_ic_public_play') : $r('sys.media.ohos_ic_public_pause'))
.width(24)
.aspectRatio(1)
.onClick(() => {
if (!this.playing) {
this.startPlay()
} else {
this.stopPlay()
}
})
Progress({ value: this.value, total: this.total })
.layoutWeight(1)
.margin({ top: 20, bottom: 20 })
}
.width('80%')
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.backgroundColor($r('app.color.white'))
.onDisAppear(() => {
this.stopPlay()
})
}
}
2)绑定全屏模态框
@Builder
PlayerBuilder () {
Column(){
AudioPlayer({ item: this.currentItem })
}
}
.width('100%')
.height('100%')
.bindContentCover($$this.isShow, this.PlayerBuilder())
AudioItemComp({
item: item
})
.onClick(() => {
this.currentItem = item
this.isShow = true
})