Flutter简单聊天界面布局及语音录制播放

目录

前言:

 注意事项:

用到的部分组件依赖及版本:

遇到的坑 

遇到的坑1:

 遇到的坑2:

遇到的坑3:

遇到的坑4:

Fluuter语音录制及播放组件生命周期

Flutter录音组件生命周期图:

 Flutter语音播放组件生命周期图:

代码

简单视频演示: 


前言:

有好多todo没实现,这里总结一下这两天遇到的坑及简单的聊天界面布局和语音录制和播放功能,这里只实现了ios端的语音录制播放功能,android端没有测试。

 注意事项:

ios端需要开启访问麦克风权限,位置在ios->Runner->Info.plist

    <key>NSMicrophoneUsageDescription</key>
    <string>访问麦克风</string>

用到的部分组件依赖及版本:

  #语音录制、播放插件
  flutter_sound: ^9.2.13
  #检查权限
  permission_handler: ^6.0.1
  #此插件会告知操作系统您的音频应用程序的性质(例如游戏、媒体播放器、助手等)以及您的应用程序将如何处理和启动音频中断(例如电话中断)
  audio_session: ^0.1.10
  #uuid
  uuid: ^3.0.6

遇到的坑 

遇到的坑1

聊天消息布局不满一页在上方显示,满一页则停留在最底部:

解决方法

使用listview反转设置可以一直保持消息在底部,但是消息数据必须要倒序;

使用Container的向上居中可以使子元素撑不满一屏时向上显示。

 遇到的坑2

在iPhoneX及所有刘海屏Bottom留白问题:

解决方法

使用SafeArea安全组件可解决此问题

遇到的坑3

IOS端在Xcode Build时报错:

Undefined symbols for architecture arm64:
"___gxx_personality_v0", referenced from:
+[FlutterSound registerWithRegistrar:] in flutter_sound(FlutterSound.o)

 解决方法

在Xcode Build Setting中的Other Linker Flags添加-lc++即可

遇到的坑4

点击录音按钮不提示申请权限直接报错:

排查了好久原来是检查权限工具版本的bug,改为6.0.1可成功弹出权限申请 

Fluuter语音录制及播放组件生命周期

Flutter录音组件生命周期图

Flutter录音组件生命周期图

 Flutter语音播放组件生命周期图

Flutter语音播放组件生命周期图

代码

import 'dart:math';
import 'dart:ui';

import 'package:audio_session/audio_session.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:new_chat/code/message_type.dart';
import 'package:new_chat/r.dart';
import 'package:new_chat/service/screen_adapter.dart';
import 'package:new_chat/util/time_utils.dart';
import 'package:new_chat/widget/toast_widget.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:logger/logger.dart' show Level;
import 'package:uuid/uuid.dart';

class SingleChatPage extends StatefulWidget {
  final String chatId;

  const SingleChatPage({Key? key, required this.chatId}) : super(key: key);

  @override
  State<SingleChatPage> createState() {
    return _SingleChatPageState();
  }
}

class _SingleChatPageState extends State<SingleChatPage> {
  //message data
  List _messageData = [];

  ///语音播放及录制定义begin
  //默认语音录制为关闭
  bool _keyboardVoiceEnable = false;

  //listview跳转控制器
  final ScrollController _scrollController = ScrollController();

  //消息文本控制器
  final TextEditingController _textEditingController = TextEditingController();

  //语音类型
  final AudioSource _theSource = AudioSource.microphone;

  //存储录音编码格式
  Codec _codec = Codec.aacMP4;

  //播放器权限
  bool _voicePlayerIsInitialized = false;

  //录制权限
  bool _voiceRecorderIsInitialized = false;

  //播放器是否可播放
  bool _voicePlayerIsReady = false;

  //播放器是否在播放
  bool _voicePlayerIsPlay = false;

  //语音播放工具
  final FlutterSoundPlayer _voicePlayer =
      FlutterSoundPlayer(logLevel: Level.error);

  //语音录制工具
  final FlutterSoundRecorder _voiceRecorder =
      FlutterSoundRecorder(logLevel: Level.error);

  //存储文件后缀
  String _voiceFilePathSuffix = 'temp_file.mp4';

  //录音文件存储前缀
  String _voiceFilePrefix = "";

  ///语音播放及录制定义end

  @override
  void initState() {
    _initMessageData();
    //初始化播放器
    _voicePlayer.openPlayer().then((value) {
      setState(() {
        _voicePlayerIsInitialized = true;
      });
    });
    //初始化录音
    _initVoiceRecorder().then((value) {
      setState(() {
        _voiceRecorderIsInitialized = true;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    //关闭语音播放
    _voicePlayer.closePlayer();
    //关闭语音录制
    _voiceRecorder.closeRecorder();
    super.dispose();
  }

  ///录音及语音方法定义begin
  ///初始录音
  ///todo 用户禁止语音权限提示
  Future<void> _initVoiceRecorder() async {
    if (!kIsWeb) {
      var status = await Permission.microphone.request();
      if (status != PermissionStatus.granted) {
        throw RecordingPermissionException('Microphone permission not granted');
      }
    }
    await _voiceRecorder.openRecorder();
    if (!await _voiceRecorder.isEncoderSupported(_codec) && kIsWeb) {
      _codec = Codec.opusWebM;
      _voiceFilePathSuffix = 'tau_file.webm';
      if (!await _voiceRecorder.isEncoderSupported(_codec) && kIsWeb) {
        _voiceRecorderIsInitialized = true;
        return;
      }
    }
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration(
      avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
      avAudioSessionCategoryOptions:
          AVAudioSessionCategoryOptions.allowBluetooth |
              AVAudioSessionCategoryOptions.defaultToSpeaker,
      avAudioSessionMode: AVAudioSessionMode.spokenAudio,
      avAudioSessionRouteSharingPolicy:
          AVAudioSessionRouteSharingPolicy.defaultPolicy,
      avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
      androidAudioAttributes: const AndroidAudioAttributes(
        contentType: AndroidAudioContentType.speech,
        flags: AndroidAudioFlags.none,
        usage: AndroidAudioUsage.voiceCommunication,
      ),
      androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
      androidWillPauseWhenDucked: true,
    ));
    _voiceRecorderIsInitialized = true;
  }

  ///开始录音并返回录音文件前缀
  String _beginVoice() {
    if (!_voiceRecorderIsInitialized) {
      ToastWidget.showToast("没有录音权限", ToastGravity.CENTER);
      throw Exception("没有录音权限");
    }
    var uuid = const Uuid().v4();
    _voiceRecorder
        .startRecorder(
            codec: _codec,
            toFile: uuid + _voiceFilePathSuffix,
            audioSource: _theSource)
        .then((value) {
      setState(() {
        //播放按钮禁用并插入语音到消息中
        _voicePlayerIsReady = false;
      });
    });
    return uuid;
  }

  ///停止录音 并将消息存储
  void _stopVoice(String voiceFileId) async {
    await _voiceRecorder.stopRecorder().then((value) {
      setState(() {
        //可以播放
        _voicePlayerIsReady = true;
        Map data = {};
        data['messageId'] = voiceFileId;
        //todo 差语音时长
        data['message'] = "语音消息按钮...";
        data['messageType'] = MessageType.voice;
        data['messageTime'] = TimeUtils.getFormatDataString(
            DateTime.now(), "yyyy-MM-dd HH:mm:ss");
        data['isMe'] = Random.secure().nextBool();
        //存储路径
        data['messageVoice'] = voiceFileId + _voiceFilePathSuffix;
        _messageData.insert(0, data);
      });
    });
  }

  ///开始播放录音
  void _beginPlayer(String messageVoiceFilePath) {
    assert(_voicePlayerIsInitialized &&
        _voicePlayerIsReady &&
        _voiceRecorder.isStopped &&
        _voicePlayer.isStopped);
    _voicePlayer
        .startPlayer(
            fromURI: messageVoiceFilePath,
            //codec: kIsWeb ? Codec.opusWebM : Codec.aacADTS,
            //语音播放完后的动作->停止播放
            whenFinished: () {
              setState(() {
                print("播放完的动作");
                _voicePlayerIsPlay = false;
                _voicePlayerIsReady = true;
              });
            })
        .then((value) {
      //语音正在播放的动作->正在播放
      setState(() {
        print("语音正在播放的动作");
        _voicePlayerIsPlay = true;
        _voicePlayerIsReady = false;
      });
    });
  }

  ///停止播放声音
  void _stopPlayer() {
    _voicePlayer.stopPlayer().then((value) {
      setState(() {
        _voicePlayerIsReady = true;
        _voicePlayerIsPlay = false;
      });
    });
  }

  ///录音及语音方法定义end

  ///初始化聊天数据
  //todo 差网络请求聊天数据 这里暂时mock
  _initMessageData() async {
    Dio dio = Dio();
    //mock data
    try {
      //todo timeout 1 seconds
      var response = await dio
          .get(
            "http://192.168.10.15:3000/mock/313/message",
          )
          .timeout(const Duration(seconds: 1));
      setState(() {
        _messageData = response.data['data'];
      });
    } catch (e) {
      //mock test data
      List<Map> tempData = [];
      Map data = {};
      data['messageId'] = const Uuid().v4();
      data['message'] = "嗯,没问题。明天我起床就联系你。";
      data['isMe'] = false;
      data['messageTime'] = "2022-08-17 16:20:20";
      data['messageType'] = MessageType.text;
      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();
      data['message'] = "好的。有什么事情及时联系我都在线的。";
      data['isMe'] = true;
      data['messageTime'] = "2022-08-17 16:19:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "晚安!";
      data['isMe'] = false;
      data['messageTime'] = "2022-08-17 16:16:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "嗯,今晚好好休息!";
      data['isMe'] = false;
      data['messageTime'] = "2022-08-17 16:15:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "好的,那到时见!!!";
      data['isMe'] = true;
      data['messageTime'] = "2022-08-16 01:15:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "不用准备什么东西,我都已经准备好了。应该是吃完午餐就出发吧。大概下午2点左右。";
      data['isMe'] = false;
      data['messageTime'] = "2022-08-16 01:13:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "需要准备什么东西带过去";
      data['isMe'] = true;
      data['messageTime'] = "2022-08-15 12:42:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "好的,10点左右可以的。你打算几点出发?";
      data['isMe'] = true;
      data['messageTime'] = "2022-08-14 14:24:20";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "嗯,大概上午10点左右吧。 如果没空就下午。";
      data['isMe'] = false;
      data['messageTime'] = "2022-08-13 11:11:22";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "明天什么时候呢???";
      data['isMe'] = true;
      data['messageTime'] = "2022-08-12 10:32:11";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      data = {};
      data['messageId'] = const Uuid().v4();

      data['message'] = "你明天有空过来吗??";
      data['isMe'] = false;
      data['messageTime'] = "2022-08-12 10:31:24";
      data['messageType'] = MessageType.text;

      tempData.add(data);
      setState(() {
        _messageData = tempData;
      });
    }
  }

  // chat widget
  //todo 后期需要根据messageSendType区分
  Widget _chatWidget(Map data) {
    if (data['isMe']) {
      return _myMessageWidget(data);
    }
    return _yourMessageWidget(data);
  }

  //your message widget
  Widget _yourMessageWidget(Map data) {
    String messageType = data['messageType'];
    return Padding(
      padding: EdgeInsets.fromLTRB(ScreenAdapter.width(32),
          ScreenAdapter.height(20), 0, ScreenAdapter.height(20)),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          Container(
            constraints: BoxConstraints(
              maxWidth: ScreenAdapter.width(450),
            ),
            padding: EdgeInsets.fromLTRB(
                ScreenAdapter.width(20),
                ScreenAdapter.height(24),
                ScreenAdapter.width(20),
                ScreenAdapter.height(24)),
            decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(ScreenAdapter.width(20)),
                color: const Color.fromRGBO(255, 255, 255, 1),
                boxShadow: const [
                  BoxShadow(
                      color: Color.fromRGBO(0, 0, 0, 0.07),
                      offset: Offset(0, 4),
                      blurRadius: 8,
                      spreadRadius: 0)
                ]),
            child: messageType == MessageType.text
                ? Text(
                    data['message'],
                    style: TextStyle(
                        color: const Color.fromRGBO(51, 51, 51, 1),
                        fontSize: ScreenAdapter.size(28)),
                  )
            //todo 差语音样式
                : GestureDetector(
                    onTap: () {
                      //如果可播放且没有在播放则播放
                      if (_voicePlayerIsReady && !_voicePlayerIsPlay) {
                        _beginPlayer(data['messageVoice']);
                      }
                      //如果可播放且在播放 则停止播放
                      if (_voicePlayerIsReady && _voicePlayerIsPlay) {
                        _stopPlayer();
                      }
                    },
                    child: Text(
                      "语音消息....",
                      style: TextStyle(
                          color: const Color.fromRGBO(51, 51, 51, 1),
                          fontSize: ScreenAdapter.size(28)),
                    ),
                  ),
          ),
          Padding(
            padding: EdgeInsets.only(left: ScreenAdapter.width(20)),
            child: Text(
              TimeUtils.setMessageTime(data['messageTime']),
              style: TextStyle(
                  color: const Color.fromRGBO(183, 183, 183, 1),
                  fontSize: ScreenAdapter.size(20),
                  fontWeight: FontWeight.w500),
            ),
          )
        ],
      ),
    );
  }

  //my message widget
  Widget _myMessageWidget(Map data) {
    String messageType = data['messageType'];
    return Padding(
        padding: EdgeInsets.fromLTRB(0, ScreenAdapter.height(20),
            ScreenAdapter.width(32), ScreenAdapter.height(20)),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Padding(
              padding: EdgeInsets.only(right: ScreenAdapter.width(20)),
              child: Text(
                TimeUtils.setMessageTime(data['messageTime']),
                style: TextStyle(
                    color: const Color.fromRGBO(183, 183, 183, 1),
                    fontSize: ScreenAdapter.size(20),
                    fontWeight: FontWeight.w500),
              ),
            ),
            Container(
              constraints: BoxConstraints(
                maxWidth: ScreenAdapter.width(450),
              ),
              //message
              padding: EdgeInsets.fromLTRB(
                  ScreenAdapter.width(20),
                  ScreenAdapter.height(24),
                  ScreenAdapter.width(20),
                  ScreenAdapter.height(24)),
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(ScreenAdapter.width(20)),
                  gradient: const LinearGradient(
                    begin: Alignment.bottomCenter,
                    end: Alignment.topCenter,
                    colors: [
                      Color.fromRGBO(99, 133, 230, 1),
                      Color.fromRGBO(179, 106, 232, 1),
                    ],
                  ),
                  boxShadow: const [
                    BoxShadow(
                        color: Color.fromRGBO(111, 129, 230, 0.2),
                        offset: Offset(0, 4),
                        blurRadius: 8,
                        spreadRadius: 0)
                  ]),
              //todo 后期要使用switch 这里先解决文本和语音
              child: messageType == MessageType.text
                  ? Text(
                      data['message'],
                      style: TextStyle(
                          color: const Color.fromRGBO(255, 255, 255, 1),
                          fontSize: ScreenAdapter.size(28)),
                    )
              //todo 差语音样式
                  : GestureDetector(
                      onTap: () {
                        //如果可播放且没有在播放则播放
                        if (_voicePlayerIsReady && !_voicePlayerIsPlay) {
                          _beginPlayer(data['messageVoice']);
                        }
                        //如果可播放且在播放 则停止播放
                        if (_voicePlayerIsReady && _voicePlayerIsPlay) {
                          _stopPlayer();
                        }
                      },
                      child: Text(
                        "语音消息....",
                        style: TextStyle(
                            color: const Color.fromRGBO(255, 255, 255, 1),
                            fontSize: ScreenAdapter.size(28)),
                      ),
                    ),
            ),
          ],
        ));
  }

  @override
  Widget build(BuildContext context) {
    ScreenAdapter.init(context);
    return Scaffold(
      body: Column(
        children: [
          //head
          Container(
            height: ScreenAdapter.height(220),
            width: ScreenAdapter.width(750),
            //padding only top->status bar
            padding: EdgeInsets.only(
                top: MediaQueryData.fromWindow(window).padding.top),
            //setting LinearGradient
            decoration: const BoxDecoration(
                boxShadow: [
                  BoxShadow(
                    offset: Offset(0, 8),
                    blurRadius: 28,
                    spreadRadius: 0,
                    color: Color.fromRGBO(60, 70, 74, 0.3),
                  )
                ],
                gradient: LinearGradient(
                  begin: Alignment.bottomCenter,
                  end: Alignment.topCenter,
                  colors: [
                    Color.fromRGBO(99, 133, 230, 1),
                    Color.fromRGBO(179, 106, 232, 1),
                  ],
                )),
            //head widget
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                //left row
                Row(
                  children: [
                    //break menu
                    Container(
                      margin: EdgeInsets.only(left: ScreenAdapter.width(44)),
                      child: InkWell(
                        onTap: () {
                          Navigator.pop(context);
                        },
                        child: Image.asset(R.assetsImgLeftMenu,
                            height: ScreenAdapter.height(42),
                            width: ScreenAdapter.width(25)),
                      ),
                    ),
                    //user portrait
                    Container(
                      margin: EdgeInsets.only(left: ScreenAdapter.width(27)),
                      child: ClipOval(
                        child: Image.network(
                          "https://img2.baidu.com/it/u=2518930323,4285282159&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800",
                          width: ScreenAdapter.width(80),
                          height: ScreenAdapter.height(80),
                          fit: BoxFit.cover,
                        ),
                      ),
                    )
                  ],
                ),
                //center column
                Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text("Shakibul Islam",
                        style: TextStyle(
                            color: const Color.fromRGBO(255, 255, 255, 1),
                            fontSize: ScreenAdapter.size(32))),
                    Text("最近会话 8:00",
                        style: TextStyle(
                            color: const Color.fromRGBO(255, 255, 255, 1),
                            fontSize: ScreenAdapter.size(24))),
                  ],
                ),
                //right row
                Row(
                  children: [
                    Container(
                      margin: EdgeInsets.only(right: ScreenAdapter.width(40)),
                      width: ScreenAdapter.width(38),
                      height: ScreenAdapter.height(24),
                      child: Image.asset(R.assetsImgChatVideo),
                    ),
                    Container(
                      margin: EdgeInsets.only(right: ScreenAdapter.width(40)),
                      child: Image.asset(R.assetsImgChatPhone,
                          width: ScreenAdapter.width(32),
                          height: ScreenAdapter.height(32)),
                    ),
                    Container(
                      margin: EdgeInsets.only(right: ScreenAdapter.width(48)),
                      child: Image.asset(R.assetsImgChatGroup,
                          width: ScreenAdapter.width(8),
                          height: ScreenAdapter.height(36)),
                    )
                  ],
                )
              ],
            ),
          ),
          //chat listview
          //todo 差消息撤回、删除、多选删除
          Expanded(
            flex: 1,
            child: Container(
                alignment: Alignment.topCenter,
                color: const Color.fromRGBO(244, 243, 249, 1),
                child: MediaQuery.removePadding(
                  removeTop: true,
                  removeBottom: true,
                  context: context,
                  child: ListView.builder(
                      shrinkWrap: true,
                      reverse: true,
                      controller: _scrollController,
                      itemCount: _messageData.length,
                      itemBuilder: (BuildContext context, int index) {
                        return _chatWidget(_messageData[index]);
                      }),
                )),
          ),
          //bottom TextField
          //set bottom color
          ColoredBox(
            color: const Color.fromRGBO(244, 243, 249, 0.5),
            child: SafeArea(
                top: false,
                left: false,
                right: false,
                // maintainBottomViewPadding: true,
                child: Padding(
                  padding: EdgeInsets.fromLTRB(
                      ScreenAdapter.width(32),
                      ScreenAdapter.height(20),
                      ScreenAdapter.width(25),
                      ScreenAdapter.height(20)),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      //TextField SizedBox
                      SizedBox(
                          width: ScreenAdapter.width(484),
                          //TextField BoxDecoration
                          child: TextField(
                            minLines: 1,
                            maxLines: 3,
                            //发送按钮
                            textInputAction: TextInputAction.send,
                            controller: _textEditingController,
                            //键盘弹起聊天页面滚到底部
                            onTap: () {
                              _scrollController.jumpTo(0);
                            },
                            onSubmitted: (String str) {
                              _textEditingController.clear();
                              if (str.isNotEmpty) {
                                setState(() {
                                  Map data = {};
                                  data['messageId'] = const Uuid().v4();
                                  data['messageType'] = MessageType.text;
                                  data['message'] = str;
                                  data['messageTime'] =
                                      TimeUtils.getFormatDataString(
                                          DateTime.now(),
                                          "yyyy-MM-dd HH:mm:ss");
                                  data['isMe'] = Random.secure().nextBool();
                                  _messageData.insert(0, data);
                                });
                              }
                            },
                            //带外边框的样式
                            decoration: InputDecoration(
                                filled: true,
                                fillColor: Colors.white,
                                hintText: "输入信息......",
                                contentPadding: const EdgeInsets.all(10),
                                suffixIcon: GestureDetector(
                                  //长摁录音
                                  onLongPress: () {
                                    setState(() {
                                      _keyboardVoiceEnable =
                                          !_keyboardVoiceEnable;
                                    });
                                    //调取录音方法
                                    if (_voicePlayerIsInitialized) {
                                      _voiceFilePrefix = _beginVoice();
                                    }
                                  },
                                  onLongPressUp: () async {
                                    //停止录音并写入消息
                                    if (_voicePlayerIsInitialized) {
                                      _stopVoice(_voiceFilePrefix);
                                    }
                                    setState(() {
                                      _keyboardVoiceEnable =
                                          !_keyboardVoiceEnable;
                                    });
                                    //jumpToBottom
                                    _scrollController.jumpTo(0);
                                  },
                                  child: Icon(
                                    Icons.keyboard_voice,
                                    color: _keyboardVoiceEnable
                                        ? Colors.blue
                                        : Colors.black26,
                                  ),
                                ),
                                //获得焦点时的边框样式
                                focusedBorder: OutlineInputBorder(
                                    borderRadius: BorderRadius.all(
                                        Radius.circular(
                                            ScreenAdapter.width(40))),
                                    borderSide: BorderSide(
                                        color: const Color.fromRGBO(
                                            99, 133, 230, 1),
                                        width: ScreenAdapter.width(4))),
                                //允许编辑焦点时的边框样式
                                enabledBorder: OutlineInputBorder(
                                    borderRadius: BorderRadius.all(
                                        Radius.circular(
                                            ScreenAdapter.width(40))),
                                    borderSide: BorderSide(
                                        color: const Color.fromRGBO(
                                            99, 133, 230, 1),
                                        width: ScreenAdapter.width(4)))),
                          )),
                      Expanded(
                          child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        children: const [
                          //todo 差相机按钮事件
                          Icon(
                            Icons.camera_alt_outlined,
                            color: Color.fromRGBO(164, 175, 207, 1),
                          ),
                          //todo 差相册按钮事件
                          Icon(
                            Icons.photo,
                            color: Color.fromRGBO(164, 175, 207, 1),
                          ),
                          //todo 还没想好
                          Icon(
                            Icons.add_circle,
                            color: Colors.blue,
                          ),
                        ],
                      ))
                    ],
                  ),
                )),
          )
          //chat widget
          //chat list
        ],
      ),
    );
  }
}

简单视频演示: 

Flutter简单聊天界面布局及语音录制播放配套视频

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...