【IM】flutter 仿微信发送语音按钮和页面

1,564 阅读3分钟

仿微信发送语音按钮、录音以及动画

Frist

这边文章的来源,来自工作中开发涉及到的IM语音发送的功能需求,因为学习flutter学艺不精,起初不知道如何来构建这样的功能

主要用到的第三方库

  • permission_handler : 获取设备的运行时权限,获取语音录制的权限

  • loading_indicator : 一个开箱即用的加载动画库,用来在录音时播放对应的动画

  • flutter_sound : 用来录制和播放声音。

  • just_audio : 用来获取音频文件时长.

  • path_provider : 用于查找文件系统上的常用位置。

  • lottie : 播放音频播放时的动画

先看效果

image image2

主要功能代码

  1. 录制语音的弹窗

通过OverlayEntry来构建弹窗

createOverlay() {
  overlayEntry = OverlayEntry(
    builder: (BuildContext context) {
      return ValueListenableBuilder<bool>(
        valueListenable: canCancelNotifier,
        builder: (_, canCancel, child) {
          return Positioned(
            child: Scaffold(
              backgroundColor: Colors.white.withOpacity(0.2),
              body: Column(
                verticalDirection: VerticalDirection.up,
                children: [
                  ClipPath(
                    clipper: TopClipper(),
                    child: Container(
                      color: canCancel ? Colors.grey : const Color(0xFFE1E1E1),
                      height: bezierBoxHeight,
                    ),
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                  Text(canCancel ? "松开 取消" : "松开 发送"),
                  const SizedBox(height: 20),
                  Container(
                    key: _cancelBtnKey,
                    height: 60,
                    width: 60,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: canCancel ? Colors.grey : const Color(0xFFE1E1E1),
                    ),
                    child: Icon(
                      Icons.close,
                      color: canCancel ? Colors.white : Colors.black,
                    ),
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                  const WechatSoundBubbleView()
                ],
              ),
            ),
          );
        },
      );
    },
  );
}
  1. 按钮

使用GestrueDetector中的相关长按事件,

  • onLongPressDown: 长按按下时,创建并展示OverlayEntry 构建的弹窗
  • onLongPressUp : 松开长按时,隐藏并销毁OverlayEntry
  • onLongPressStart : 长按开始时,开始录制语音。
  • onLongPressEnd : 长按结束时,停止录音,并通过回调把录音文件的路径,传至上层组件
  • onLongPressMoveUpdate : 监听长按开始后的手指移动的位移偏移量,以计算手指是否在取消按钮的区域内。

代码:

GestureDetector(
  child: Container(
    width: double.infinity,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(12),
      color: Colors.white,
    ),
    margin: const EdgeInsets.symmetric(horizontal: 32),
    child: const Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text("按住说话"),
      ],
    ),
  ),
  onLongPressDown: (LongPressDownDetails details) {
    debugPrint("long press down");
    createOverlay();
    initRecorder();
  },
  onLongPressUp: () {
    cancelPosition = null;
    cancelSize = null;
  },
  onLongPressStart: (LongPressStartDetails details) async {
    showFloatingButtonOverlay(context);
    await startRecorder();
  },
  onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
    final globalPosition = details.globalPosition;
    getCancelBtnInfo();
    if (cancelSize != null && cancelPosition != null && cancelSize != Size.zero && cancelPosition != Offset.zero) {
      if (globalPosition.dx >= cancelPosition!.dx &&
          globalPosition.dx <= cancelPosition!.dx + cancelSize!.width &&
          globalPosition.dy >= cancelPosition!.dy &&
          globalPosition.dy <= cancelPosition!.dy + cancelSize!.height) {
        canCancelNotifier.value = true;
      } else {
        canCancelNotifier.value = false;
      }
    }
  },
  onLongPressEnd: (LongPressEndDetails details) {
    overlayEntry.remove();
    // 停止录音
    stopRecording();
    // disposeRecorder();
  },
);

canCancelNotifier用来通知更新视图,修改 “松开发送”、“松开取消”,以及按钮颜色等参数。

getCancelBtnInfo()方法用来获取 取消按钮在屏幕中的位置:

getCancelBtnInfo() {
  final RenderBox? renderBox = _cancelBtnKey.currentContext?.findRenderObject() as RenderBox?;
  if (cancelPosition == null || cancelPosition == Offset.zero) {
    final position = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
    cancelPosition = position;
  }
  if (cancelSize == null || cancelSize == Size.zero) {
    cancelSize = renderBox?.size;
  }
}
  1. 开始录音和结束录音

使用的flutter_sound 第三方库来进行录音,

  • 开始:
Future<void> startRecorder() async {
  try {
    final status = await Permission.microphone.request();
    if (status != PermissionStatus.granted) {
      print('microphone not has Permission ');
      return;
    }
    await recorder.openRecorder();
    soundFilePath =
        "${(await getTemporaryDirectory()).path}/wechat_sound/sound_${DateTime.now().millisecondsSinceEpoch}${ext[Codec.pcm16WAV.index]}";
    final file = File(soundFilePath);
    if (!(await file.exists())) {
      await file.create(recursive: true);
    }
    await recorder.openRecorder();
    print('Recording started at path $soundFilePath');
    await recorder.startRecorder(
      toFile: soundFilePath,
      codec: Codec.pcm16WAV,
      numChannels: 1,
      bitRate: 8000,
    );
  } catch (err) {
    print(err);
  }
}
  • 结束:
Future<void> stopRecording() async {
  try {
    await recorder.stopRecorder();
    if (!canCancelNotifier.value) {
      widget.onRecordedCallback?.call(soundFilePath);
    }
    print('Recording stopped');
  } catch (error) {
    print('error stoped recording $error');
  }
}

总结

本身功能不难,只是之前学习的时候解除OverlayEntry比较少,项目中用到的Dialog之类的组件都是用的现成的第三方库,无脑拿来就使,又遇上这个组件网上资源比较少,导致对这方面知识欠缺。这次相当于补齐了一部分关于Overlay相关的知识。

代码放在了github /lib/pages/wechat_sound下,欢迎指正。

github传送门