更新记录
0.5.1(2025-03-27)
- 同步bsx-recorder-api 版本 0.5.5 支持发送wav格式(一般语音聊天不建议aac)
 - 修复小问题
 
0.5.0(2024-12-31)
首发版本 v 0.5.0
平台兼容性
uni-app x
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 | 
|---|---|---|---|---|---|
| - | - | 5.0 | × | × | × | 
bsx-voicechat-wx 一款类微信发语音的页面组件,适用于移动端聊天发语音
重要
- 此组件基于bsx-recorder-api插件开发,使用前需先购买插件.bsx-recorder-api
 - 暂仅支持Android 端
 - 因使用bsx-recorder-api, 故需要打自定义基座
 - 除录音和播放操作外,本组件使用uts相关api的能力,放心使用.
 
开发文档
作者
aoaobaba 傲傲爸爸~
介绍
基于bsx-recorder-api 开发的类微信发语音界面。
功能
- [X] 长按唤起语音界面说话录音
 - [X] 滑动到响应的位置指定操作
 - [X] 取消语音
 - [X] 播放已录制语音
 - [X] 松手发送语音
 - [X] 手动发送语音
 - [X] 短时间录音无效
 
说明
本组件插件依赖bsx-recorder-api, 使用前请先购买授权并导入项目。 组件不能脱离bsx-recorder-api 单独使用!!
布局:以下是组件在项目页面中的位置示意。可以看到,组件需要放置在一个dialogPage里;
dialogPage用于遮罩整个app,起到遮罩层的功能。

事件:app的page里面需要设置用户的touch操作相关的事件,并触发组件专有的事件;dialogPage里面监听事件后再调用组件暴露的专有事件,组件在接收到处理请求时 开始处理相应的业务逻辑。例如,用户touchmove事件时,组件会根据手指的移动位置,分别响应处理【取消】或【更多操作】;当组件处理了事件后,dialogPage里可以 callback再进行处理。例如,在组件中提供了@closeCallback,用于关闭时在dialogPage内提供更多操作,关闭时会返回录音文件全路径

注意!!本组件没有提供dialogPage, 但是依赖一个承载的父页面,你可以把组件直接放在Page里,但那样效果很糟糕。 下面示例会提供作者项目内的dialogPage相关代码,以供参考!
类型
type bsxVoiceChatWxComUiStyle = {
    visualizerViewBackgroundColor?: string  // 可视化视图背景色
    visualizerBaseColor?: string //可视化颜色
}
type bsxVoiceChatWxComFuntions = {
    enableAudioVisualizer?: boolean | null  //是否启用音频实时 可视化
    enableSilenceHandle?: boolean | null   // 是否启用静音处理
    maxDuration?: number | null  // 最大录音时长,默认最大30分钟 超过自动停止
    silenceThreshold?: number | null  // 静音检测阈值 
    silenceThresholdMs?: number | null  // 静音检测持续时长阈值
    fileDirectoryName? : string | null  //文件存储目录 如果空 则创建在BsxRecorder 目录下
    fileNameSuffix? : string | null  // 录音aac文件名前缀 完整的文件名为前缀_日期时间  默认bsx_recorder
    fileType: string // 录音文件类型 支持 aac和wav  未指定或未匹配的默认aac
}
组件参数
| 属性 | 类型 | 默认值 | 功能 | 
|---|---|---|---|
| pageInstance | UniPage | 必须传 | 组件的父页面实例 | 
| uiStyle | bsxVoiceChatWxComUiStyle | 参考类型 | 组件UI | 
| comFunctions | bsxVoiceChatWxComFuntions | 参考类型 | 组件功能性属性 | 
| @closeCallback | callback: () => UTSJSONObject | 可选 | 组件关闭时的回调里面包含了3种action值,分别为cancel,send,tooShort | 
| @onStartRecord | .. | 可选 | 开始录音回调 | 
| @onRecording | .. | 可选 | 录音中回调 | 
| @onFailRecord | .. | 可选 | 录音失败回调 | 
| @onStopRecord | .. | 可选 | 录音停止回调 | 
| @onStartPlay | .. | 可选 | 开始播放回调 | 
| @onStopPlay | .. | 可选 | 停止播放回调 | 
| @onEndPlay | .. | 可选 | 播放完回调 | 
组件方法
通过组件的Ref 引用,可以调用!
| 方法名 | 参数 | 功能 | 调用示例 | 
|---|---|---|---|
| show | 无 | 组件展现 | Ref.value?.show?.() | 
| close | 无 | 关闭组件 | |
| actionsAnimatingByTouchMoveEvent | (screenX, screenY) | 处理touchmove事件 | |
| actionsConsumeTouchEndEvent | (screenX, screenY) | 处理touchend事件 | 例如松开发送,或指定区域的操作 | 
| actionsConsumeTouchCancelEvent | 处理意外中断touch事件 | 例如系统行为中断了用户的touch事件时触发 | 
使用
- 新建一个页面并注册,作为dialogPage使用,用做遮罩层
 - 在dialogPage里面放置bsx-voicechat-wx组件,并设置相关属性或回调处理
 - 在业务页面响应用户操作并打开dialogPage显示组件,业务页面需要触发touch相关的组件专用事件!
 
步骤一、二 新增的页面并注册到page.json 用于dialogPage
<template>
    <!-- !!遮罩层 当前页是一个dialogpage 页面 用于展现BsxRecorderView 组件时 的应用界面遮罩
    * https://doc.dcloud.net.cn/uni-app-x/api/dialog-page.html
    * 1. 初始化时 给了全屏的宽高
    * 2. 设置了背景色 background-color: rgba(0, 0, 0, .82)
    * 3. 其父页面为调用打开当前dialog的页面
    * 4. bsx-voicechat-wx 组件放置在此dialog page 内
    * 5. 当使用类wechat的触摸操作语音的模式时,注意!!!!父组件的touchmove和touchend 事件触发是必要的!因为组件需要响应触摸处理!
    * 6. 与父页面的通信 使用事件总线!如果添加了事件触发和订阅,请在适当时机off掉!
    *  
    * 更多信息请参阅组件插件的开发文档
    -->
    <view id="bsx-recorder-demo"  class="" style="background-color: rgba(72, 72, 72, .96);" :style="`width:${screenWidht};height:${screenHight}`">
        <!-- bsx 仿微信语音聊天组件 -->
        <bsx-voicechat-wx ref="bsxVoicechatWxRef" :pageInstance="dialogPage"  :uiStyle ="recorderUi" :comFunctions="recorderFun" @closeCallback="closePage"
            @onStartRecord="onStartRecord" @onStopRecord = "onStopRecord"
        ></bsx-voicechat-wx>
    </view>
</template>
<script setup lang="uts">
    import {bsxVoiceChatWxComUiStyle, bsxVoiceChatWxComFuntions } from '@/uni_modules/bsx-voicechat-wx/components/bsx-voicechat-wx/bsxVoiceChatWxType.uts'
    const recorderUi = ref<bsxVoiceChatWxComUiStyle>({
        visualizerViewBackgroundColor: '#95ec6a',  // #95ec6a wechat可视化语音背景色
        visualizerBaseColor: '#575D57'    // 音效柱状图颜色 支持安卓Color支持的色值: 参考   转换失败将使用默认值
        // https://developer.android.google.cn/guide/topics/resources/more-resources?hl=zh-cn#Color 参见官方Color 文档
    })
    const recorderFun = ref<bsxVoiceChatWxComFuntions>({
        enableAudioVisualizer: true,  // 是否启用实时音频可视化
        enableSilenceHandle: false, // 是否开启静音检测
        maxDuration: 30 * 60 * 1000,  // 最大录音时间 超过则自动停止录音
        silenceThreshold: 80,  // 静音检测阈值 低于此值时 silenceThresholdMs 将会作用,例如静音超过silenceThresholdMs时长 则自动停止录音!
        silenceThresholdMs: 2 * 1000 , // 静音持续时长阈值
        fileType: 'wav' // 文件类型 aac 或 wav
    })
    // 应用视图模板  可以从父页面调用当前dialogpage时通过url参数传值以切换不同模式 base or wechat
    const applyMode = 'base'
    // bsx-recorder-view 组件引用
    const bsxVoicechatWxRef = ref<BsxVoicechatWxComponentPublicInstance | null>(null)
    const screenHight = ref(0)
    const screenWidht = ref(0)
    // 当前的dialog page 实例
    const dialogPage = ref<UniPage | null>(null)
    onLoad(() => {
        console.log('设备信息', uni.getWindowInfo())
        let wi = uni.getWindowInfo()
        screenHight.value = wi.screenHeight -45
        screenWidht.value = wi.screenWidth
    })
    // 进行一些必要实例获取
    onReady(() => {
        const currentInstance = getCurrentInstance()
        dialogPage.value = currentInstance?.proxy?.$page
        console.log("dialog page 的主页面实例", dialogPage.value)
        // 打开录音机组件 这一定要稍后等待获取 dialogPage 实例完成
        setTimeout(() => {
            bsxVoicechatWxRef.value?.show?.()
        }, 300)
        // 订阅父页面的相关touchmove 事件
        uni.$on('bsxRecorderViewParentPageTouchMove', (screenX:number, screenY: number) => {
            // 组件内部会响应touch move 的changed X Y值进行相应的功能处理
            bsxVoicechatWxRef.value?.actionsAnimatingByTouchMoveEvent?.(screenX, screenY)
        })
        // 订阅父页面的touchend 事件
        uni.$on('bsxRecorderViewParentPageTouchEnd', (screenX:number, screenY: number) => {
            // 组件内部会响应touch emd 的changed X Y值进行相应的功能处理 wechat 模式时 取消发送语音 或 更多操作
            bsxVoicechatWxRef.value?.actionsConsumeTouchEndEvent?.(screenX, screenY)
        })
        // 订阅父页面的touchcancel 事件
        uni.$on('bsxRecorderViewParentPageTouchCancel', () => {
            bsxVoicechatWxRef.value?.actionsConsumeTouchCancelEvent?.()
        })
    })
    // 关闭dialog page
    const closePage = (json:UTSJSONObject) => {
        // 触发关闭的action
        console.log(`关闭事件页面:`, json)
        // 给录音机组件一个 动画处理的时间
        setTimeout(() => {
            uni.closeDialogPage({
                success: () => {
                    console.log("dialogPage 已关闭")
                },
                fail: (err) => {
                    console.log("dialogPage 关闭错误", err)
                },
                complete: (res) => {
                    console.log("dialogPage 关闭完成", res)
                }
            })
        }, 300) 
    }
    const testClick = () => {
        bsxVoicechatWxRef.value?.show?.()
    }
    const touchStart = (e: UniEvent) => {
        // console.log("触发了touchstart 事件", e)
    }
    const touchMove = (event: UniTouchEvent) => {
        console.log("触发了touchMove 事件", event.changedTouches[0])
        return true
    }
    const touchEnd = () => {
        console.log("结束触摸")
    }
    const touchCancel = () => {
        console.log("取消触摸")
    }
    const onStartRecord= () => {
        console.log('page页面接收到回调,开始录音了---------------')
    }
    const onStopRecord= (stop:UTSJSONObject) => {
        console.log('page页面接收到回调,录音停止了---------------', stop)
        // 手动停止的 不要关闭dialogpage
        if (stop['reason'] != 'manual')
        closePage({
            action: 'auto',
            filePath: stop['filePath'],
            reason: stop['reason']
        })
    }
</script>
<style>
</style>
步骤三 业务页面的处理
<template>
    <!-- #ifdef APP -->
    <scroll-view style="flex:1">
    <!-- #endif -->
        <view class="chat-box">
            <view class="" v-for="i in 3">
                <view class="bsx-chat" style="margin-top: 10;">
                    <view class="" style="margin-left: 10;border-radius: 30;height: 45;width: 45;">
                        <image src="/static/BSX_mini.png" mode="" style=" height: 45;width: 45;"></image>
                    </view>
                    <view class=""
                        style="margin-top: 15; height: 30; margin-left: 10;padding: 3;border: 1px solid gainsboro;border-radius: 20;">
                        <text style="padding: 5;font-size: 15;">BSX 专注uniapp X的Bootstrap框架。</text>
                    </view>
                </view>
                <view class="me-chat">
                    <view class=""
                        style="margin-top: 15; height:60; margin-right: 10;padding: 3;border: 1px solid gainsboro;border-radius: 20;">
                        <text style="padding: 5;font-size: 15;width: 160;">清风徐来,水波不兴;
                            好好学习,天天向上。</text>
                    </view>
                    <view class="" style="margin-right: 10;border-radius: 30;height: 45;width: 45;">
                        <image src="/static/BSX_mini.png" mode="" style=" height: 45;width: 45;"></image>
                    </view>
                </view>
            </view>
        </view>
        <view @longpress="longPress" @touchstart="touchStart" @touchcancel="touchCancel" @touchmove="touchMove" @touchend="touchEnd" class=""
            style="width: 100%;display: flex;justify-content: center; position: fixed;height: 45;bottom: 0; background-color: ghostwhite;">
            <text style="text-align: center; font-size: 16;">长按说话</text>
        </view>
    <!-- #ifdef APP -->
    </scroll-view>
    <!-- #endif -->
</template>
<script setup>
    import Manifest from 'android.Manifest'
    import ActivityCompat from 'androidx.core.app.ActivityCompat'
    import ContextCompat from 'androidx.core.content.ContextCompat'
    import PackageManager from 'android.content.pm.PackageManager'
    const touchLongTimer = ref(0)
    const touching = ref(false)
    const touchingTime = ref(0)
    onReady(() =>{
        // 申请麦克风权限
        // 检查并申请麦克风权限
        // if (ContextCompat.checkSelfPermission(UTSAndroid.getUniActivity()!, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
            // true // 权限已授予
            // 申请权限
            ActivityCompat.requestPermissions(UTSAndroid.getUniActivity()!, arrayOf(Manifest.permission.RECORD_AUDIO), (100).toInt())
        // }
    })
    onMounted(() => {
    })
    const openRecorder = () => {
        const pages = getCurrentPages()
        // 获取当前页面
        const page = pages[pages.length - 1]
        uni.openDialogPage({
            url: '/pages/bsx-recorder-wechat/bsx-recorder-wechat',
            animationType: 'fade-in',
            success: () => {
                console.log('dialog open-------------------')
            },
            fail: (res) => {
                console.log('dialog open fail--------------', res)
            },
            animationDuration: 300,
            parentPage: page
        })
    }
    const touchCancel = (event : UniTouchEvent) => {
        touching.value = false
        touchingTime.value = 0
        console.log("取消 touch")
        // 需要发送触摸事件被系统中止的事件给到组件 处理
        uni.$emit('bsxRecorderViewParentPageTouchCancel')
    }   
    const longPress = (event : UniTouchEvent) => {
        console.log("longPress", event)
        openRecorder()
    }
    const touchMove = (event : UniTouchEvent) => {
        // 触发touchmove 监听
        uni.$emit('bsxRecorderViewParentPageTouchMove', event.changedTouches[0].screenX, event.changedTouches[0].screenY)
    }
    const touchStart = () => {
        console.log('父页面 touch start ---------------------')
    }
    const touchEnd = (event : UniTouchEvent) => {
        console.log('父页面 touch end ---------------------')
        uni.$emit('bsxRecorderViewParentPageTouchEnd',  event.changedTouches[0].screenX, event.changedTouches[0].screenY)
    }
    onUnmounted(() => {
        uni.$off('bsxRecorderViewParentPageTouchMove')
        uni.$off('bsxRecorderViewParentPageTouchEnd')
    })
</script>
<style>
    .chat-box {
        display: flex;
        flex-direction: column;
    }
    .bsx-chat {
        display: flex;
        flex-direction: row;
        align-self: flex-start;
        height: 90;
    }
    .me-chat {
        display: flex;
        flex-direction: row;
        align-self: flex-end;
        height: 90;
    }
</style>
以上,为使用组件的完整示例。主要是通过dialogPage实现的应用遮罩功能,主要的区别在于组件的父组件是dialogPage 而不是你的业务页面!而业务页面变成了 dialogPage的父组件! 有关dialogPage, 请参阅官方文档dialogPage ,进一步了解。
权限
需要请求录音权限!!!
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
tips
- 重要!!!!! 需要录音权限!请配置清单。权限申请自行处理。
 - 需要自定义基座
 - 其他问题,欢迎指正
 
Happy new year ! 2025
谢谢!

                                                                    
                                                                        收藏人数:
                                    
                                                        购买普通授权版(
                                                                                                                试用
                                                    
                                        赞赏(0)
                                    
                                            
 下载 60
                
 赞赏 0
                
            
                    下载 10693743 
                
                        赞赏 1797 
                    
            
            
            
            
            
            
            
            
            
            
            
            
            
            
                        
                                赞赏
                            
            
京公网安备:11010802035340号