Flutter聊天页面

一、依赖与预览

  • get: 4.1.4
  • video_player: ^2.1.1 /// 播放视频
  • emoji_picker_flutter: ^1.0.5 /// 表情库
  • flutter_keyboard_visibility: ^5.0.3 /// 监听键盘弹起

有关IM相关的请看《Flutter融云接入部分》

预览

在这里插入图片描述

二、使用

  • _messageText:TextEditingController()类型

1. 表情部分展示

import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';

///选中表情
_onEmojiSelected(Emoji emoji) {
  _messageText
    ..text += emoji.emoji
    ..selection = TextSelection.fromPosition(
    TextPosition(offset: _messageText.text.length));
}

///表情删除按钮
_onBackspacePressed() {
  _messageText
    ..text = _messageText.text.characters.skipLast(1).toString()
    ..selection = TextSelection.fromPosition(
    TextPosition(offset: _messageText.text.length));
}

EmojiPicker(
  onEmojiSelected: (Category category, Emoji emoji) {
    /// 当表情被选择后自定义方法操作
    _onEmojiSelected(emoji);
  },
  onBackspacePressed: _onBackspacePressed,
  config: const Config(
    columns: 7,
    emojiSizeMax: 25.0,
    verticalSpacing: 0,
    horizontalSpacing: 0,
    initCategory: Category.RECENT,
    bgColor: Color(0xFFF2F2F2),
    indicatorColor: Color(0xff65DAC5),
    iconColor: Colors.orange,
    iconColorSelected: Color(0xff65DAC5),
    progressIndicatorColor: Color(0xff65DAC5),
    backspaceColor: Color(0xff65DAC5),
    showRecentsTab: true,
    recentsLimit: 28,
    noRecentsText: 'No Recents',
    noRecentsStyle:
    TextStyle(fontSize: 20, color: Colors.black26),
    categoryIcons: CategoryIcons(),
    buttonMode: ButtonMode.MATERIAL)
)

2. 初始化背景视频

import 'package:video_player/video_player.dart';

late VideoPlayerController _controller; //背景视频播放控制器

@override
void initState() {
  ///初始化视频播放
  _controller = VideoPlayerController.network(
    'https://gugu-1300042725.cos.ap-shanghai.myqcloud.com/0_szDjEDn.mp4');
  _controller.addListener(() {
    setState(() {});
  });
  _controller.setVolume(0);
  _controller.setLooping(true);
  _controller.initialize().then((_) => setState(() {}));
  _controller.play();

  super.initState();
}

@override
void dispose() {
  _controller.dispose(); //移除监听
  super.dispose();
}

@override
Widget build(BuildContext context) {
  return AnnotatedRegion<SystemUiOverlayStyle>(
    value: SystemUiOverlayStyle.light,
    child: child: Stack(
      fit: StackFit.expand,
      children: [
        _controller.value.isInitialized /// 判断是否已经初始化,如果初始化,则加载,否则显示备用图片
        ? Transform.scale(
          scale: _controller.value.aspectRatio /
          MediaQuery.of(context).size.aspectRatio,
          child: Center(
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: VideoPlayer(_controller),
            ),
          ),
        )
        : Image.asset(R.imagesHealingIconBackground),
      ],
    ),
  );
}

3. 直接贴源码

bubble.dart

import 'package:flutter/material.dart';

const _ArrowWidth = 7.0;    //箭头宽度
const _ArrowHeight = 10.0;  //箭头高度
const _MinHeight = 32.0;    //内容最小高度
const _MinWidth = 50.0;     //内容最小宽度

class Bubble extends StatelessWidget {
  final BubbleDirection direction;
  final Radius? borderRadius;
  final Widget? child;
  final BoxDecoration? decoration;
  final Color? color;
  final _left;
  final _right;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;
  final BoxConstraints? constraints;
  final double? width;
  final double? height;
  final Alignment? alignment;
  const Bubble(
      {Key? key,
        this.direction = BubbleDirection.left,
        this.borderRadius,
        this.child, this.decoration, this.color, this.padding, this.margin, this.constraints, this.width, this.height, this.alignment})
      : _left = direction == BubbleDirection.left ? _ArrowWidth : 0.0,
        _right = direction == BubbleDirection.right ? _ArrowWidth : 0.0,
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper:
      _BubbleClipper(direction, this.borderRadius ?? Radius.circular(5.0)),
      child: Container(
        alignment: this.alignment,
        width: this.width,
        height: this.height,
        constraints: (this.constraints??BoxConstraints()).copyWith(minHeight: _MinHeight,minWidth: _MinWidth),
        margin: this.margin,
        decoration: this.decoration,
        color: this.color,
        padding: EdgeInsets.fromLTRB(this._left, 0.0, this._right, 0.0).add(this.padding??EdgeInsets.fromLTRB(7.0, 5.0, 7.0, 5.0)),
        child: this.child,
      ),
    );
  }
}

///方向
enum BubbleDirection { left, right }

class _BubbleClipper extends CustomClipper<Path> {
  final BubbleDirection direction;
  final Radius radius;
  _BubbleClipper(this.direction, this.radius);

  @override
  Path getClip(Size size) {
    final path = Path();
    final path2 = Path();
    final centerPoint = (size.height / 2).clamp(_MinHeight/2, _MinHeight/2);
    print(centerPoint);
    if (this.direction == BubbleDirection.left) {
      //绘制左边三角形
      path.moveTo(0, centerPoint);
      path.lineTo(_ArrowWidth, centerPoint - _ArrowHeight/2);
      path.lineTo(_ArrowWidth, centerPoint + _ArrowHeight/2);
      path.close();
      //绘制矩形
      path2.addRRect(RRect.fromRectAndRadius(
          Rect.fromLTWH(_ArrowWidth, 0, (size.width - _ArrowWidth), size.height), this.radius));
      //合并
      path.addPath(path2, Offset(0, 0));
    } else {
      //绘制右边三角形
      path.moveTo(size.width, centerPoint);
      path.lineTo(size.width - _ArrowWidth, centerPoint - _ArrowHeight/2);
      path.lineTo(size.width - _ArrowWidth, centerPoint + _ArrowHeight/2);
      path.close();
      //绘制矩形
      path2.addRRect(RRect.fromRectAndRadius(
          Rect.fromLTWH(0, 0, (size.width - _ArrowWidth), size.height), this.radius));
      //合并
      path.addPath(path2, Offset(0, 0));
    }
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return false;
  }
}

test.dart

/// 原生
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// 第三方
import 'package:get/get.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';

///本地
import '../../../theme/utils/export.dart'; /// 主要是十六进制色制、自适应大小、静态图片
import '../../../utils/widget/bubble.dart'; /// 聊天气泡

//气球聊天详情页
// ignore: must_be_immutable
class TestChatPage extends StatefulWidget {
  TestChatPage();

  @override
  _TestChatPageState createState() => _TestChatPageState();
}

class _TestChatPageState extends State<TestChatPage> {

  /// 用户头像
  Widget userAvatar(img, size){
    return Padding(
      padding: EdgeInsets.all(10.dp),
      child: (img == null || img == "")
          ? Image.asset(
        R.imagesMineIconAvatar,
        height: size,
      )
          : CircleAvatar(
        radius: size/2,
        backgroundImage: NetworkImage(img),
      ),
    );
  }

  /// 通用简单text格式
  singleTextWeight(text, color, fontSize){
    return Text(
      text,
      style: TextStyle(
          color: color,
          fontSize: fontSize
      ),
      overflow: TextOverflow.ellipsis,
    );
  }

  /// 通用获取安全顶部距离
  Widget safePadding(BuildContext context, color){
    return Container(
      height: MediaQuery.of(context).padding.top,
      color: color,
    );
  }

  /// 隐藏键盘
  void hideKeyboard(BuildContext context){
    FocusScopeNode currentFocus = FocusScope.of(context);
    if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
      FocusManager.instance.primaryFocus!.unfocus();
    }
  }

  ScrollController scrollController = ScrollController();

  /// 消息列表
  var _messageList = Rx<List<Map<dynamic, dynamic>>>([Map()]);
  /// 判断是否首次进入页面
  var firstCome = true.obs;

  /// 输入框焦点
  FocusNode focusNode = new FocusNode();

  late VideoPlayerController _controller; //背景视频播放控制器

  var _visZhifeiji = true.obs; //发送按钮隐藏和显示
  bool _isText = true; //true文本输入  false语言输入
  final TextEditingController _messageText = new TextEditingController(); //需要初始化的时候赋值用
  bool emojiShowing = false; /// 是否显示表情
  bool keyboardShowing = false; /// 是否显示键盘

  /// 获取用户历史消息
  getHistoryMessages() async {
    _messageList.value = [
      {"messageDirection": 1, "content": "还好"},
      {"messageDirection": 2, "content": "最近还好吗?"},
      {"messageDirection": 1, "content": "是啊"},
    ];
  }

  @override
  void initState() {
    ///初始化视频播放
    _controller = VideoPlayerController.network(
        'https://gugu-1300042725.cos.ap-shanghai.myqcloud.com/0_szDjEDn.mp4');
    _controller.addListener(() {
      setState(() {});
    });
    _controller.setVolume(0);
    _controller.setLooping(true);
    _controller.initialize().then((_) => setState(() {}));
    _controller.play();

    getHistoryMessages();
    focusNode.addListener(() {
      if(focusNode.hasFocus){
        keyboardShowing = true;
        if(emojiShowing){
          setState(() {
            emojiShowing = !emojiShowing;
          });
        }
      } else {
        keyboardShowing = false;
      }
    });

    super.initState();
  }

  ///选中表情
  _onEmojiSelected(Emoji emoji) {
    _visZhifeiji.value = false;
    _messageText
      ..text += emoji.emoji
      ..selection = TextSelection.fromPosition(
          TextPosition(offset: _messageText.text.length));
  }

  ///表情删除按钮
  _onBackspacePressed() {
    _messageText
      ..text = _messageText.text.characters.skipLast(1).toString()
      ..selection = TextSelection.fromPosition(
          TextPosition(offset: _messageText.text.length));
    if(_messageText.text.length == 0){
      _visZhifeiji.value = true;
    }
  }

  @override
  void dispose() {
    _controller.dispose(); //移除监听
    scrollController.dispose();
    super.dispose();
  }

  /// 头部 Banner
  Widget _buildHeader(context) {
    return Container(
      color: Colors.transparent,
      width: double.infinity,
      height: 30.dp,
      child: Padding(
        padding: EdgeInsets.only(left: 10.dp, right: 10.dp),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Container(
              width: MediaQuery.of(context).size.width/4,
              child: Row(
                children: [
                  GestureDetector(
                    child: Image.asset(
                      R.imagesMineIconBack,
                      width: 18.dp,
                      height: 18.dp,
                      color: c_FF,
                    ),
                    onTap: ()=>Get.back(),
                  ),
                  Expanded(child: Text("")),
                ],
              ),
            ),
            Container(
              width: MediaQuery.of(context).size.width/4,
              child: Center(
                child: singleTextWeight("Jaycee", c_FF, 16.dp),
              ),
            ),
            Container(
              width: MediaQuery.of(context).size.width/4,
              child: Text(""),
            ),
          ],
        ),
      ),
    );
  }

  /// 渲染聊天内容
  next(_messageRealList, index){
    return Row(
      children: [
        _messageRealList[index]['messageDirection'] == 1 ? Expanded(child: Text("")) : userAvatar("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F202005%2F06%2F20200506110929_iajqi.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1631409536&t=03cad8232b224d6a7ff11f58ff2be920", 58.dp),
        GestureDetector(
          onTap: ()=>{
          },
          child: Container(
            constraints: BoxConstraints(
                maxWidth: MediaQuery.of(context).size.width - 128.dp
            ),
            child: Bubble(
                direction: _messageRealList[index]['messageDirection'] == 1 ? BubbleDirection.right : BubbleDirection.left,
                color: c_FF95B5,
                child: Text(
                  "${_messageRealList[index]['content']}",
                  style: TextStyle(
                      color: c_00,
                      fontSize: 18.dp
                  ),
                )
            ),
          ),
        ),
        _messageRealList[index]['messageDirection'] != 1 ? Expanded(child: Text("")) : userAvatar("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Fc8%2Fdd%2Fb9%2Fc8ddb934a69d90216f1b406cf3975475.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1631409536&t=17150dcec9e325456525160928d384f7", 58.dp),
      ],
    );
  }

  /// 渲染聊天部分
  Widget _chatList(BuildContext context){
    List<Map<dynamic, dynamic>> _messageRealList = _messageList.value.reversed.toList();
    if(scrollController.hasClients && firstCome.value && scrollController.position.maxScrollExtent != 0.0){
      scrollController.jumpTo(scrollController.position.maxScrollExtent);
      firstCome.value = false;
    }
    return Column(
      children: [
        safePadding(context, Colors.transparent),
        _buildHeader(context),
        Container(
          height: 80.dp,
          width: MediaQuery.of(context).size.width - 40.dp,
          decoration: BoxDecoration(
              color: c_FF.withOpacity(0.6),
              borderRadius: BorderRadius.circular(20.dp)
          ),
          child: Center(
            child: singleTextWeight("好久不见", c_FF, 16.dp),
          ),
        ),
        Expanded(child: _messageList.value.length > 0 ? EasyRefresh.custom(
            scrollController: scrollController,
            header: ClassicalHeader(),
            onRefresh: () async {
              /// 加载更多消息方法
            },
            slivers: [
              SliverGrid(
                delegate: SliverChildBuilderDelegate(
                      (context, index) => next(_messageRealList, index),
                  childCount: _messageList.value.length,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 1,
                  mainAxisSpacing: 10.dp,
                  crossAxisSpacing: 10.dp,
                  childAspectRatio: 312.dp / 60.dp,
                ),
              )
            ]
        ) : Text(""),),
        ///输入键盘
        Container(
          color: Color(0x0dffffff),
          margin: EdgeInsets.fromLTRB(0, 5, 0, 10),
          height: 60.dp,
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              InkWell(
                child: Container(
                  margin: EdgeInsets.fromLTRB(8, 0, 13, 0),
                  width: 34.dp,
                  height: 34.dp,
                  child: _isText
                      ? Image.asset(R.imagesChatButtonVoice)
                      : Image.asset(R.imagesChatButtonKeyboard),
                ),
                // onTap: () {
                //   if (this._isText) {
                //     this._isText = false;
                //     this.emojiShowing = false;
                //     this._visZhifeiji = true;
                //   } else {
                //     this._isText = true;
                //   }
                // },
              ),
              Expanded(
                flex: 1,
                child: _isText
                    ? TextFormField(
                  focusNode: focusNode,
                  controller: _messageText,
                  style: const TextStyle(
                      fontSize: 18, color: Colors.white),
                  decoration: InputDecoration(
                      hintText: '请输入',
                      hintStyle: TextStyle(
                          fontSize: 18, color: Color(0x80ffffff))),
                  onChanged: (value) {
                    if (value.isEmpty) {
                      _visZhifeiji.value = true;
                    } else {
                      _visZhifeiji.value = false;
                    }
                  },
                ) : Text("data"),
              ),
              InkWell(
                child: Container(
                  margin: EdgeInsets.fromLTRB(8, 0, 6, 0),
                  width: 30.dp,
                  height: 30.dp,
                  child: Image.asset(R.imagesChatButtonEmoji),
                ),
                onTap: () {
                  hideKeyboard(context);
                  Future.delayed(Duration(milliseconds: 10), (){
                    setState(() {
                      emojiShowing = !emojiShowing;
                    });
                    if(emojiShowing){
                      scrollController.jumpTo(scrollController.position.maxScrollExtent+250.dp);
                    }
                  });
                },
              ),
              InkWell(
                child: Container(
                  margin: EdgeInsets.fromLTRB(6, 0, 15, 0),
                  width: 34.dp,
                  height: 34.dp,
                  child: Image.asset(R.imagesChatButtonAdd),
                ),
                // onTap:(){
                // _onImageButtonPressed(ImageSource.camera, context: context);//打开相机
                // }
              ),
              Obx(()=>Offstage(
                offstage: _visZhifeiji.value,
                child: Container(
                  margin: EdgeInsets.fromLTRB(0, 0, 15, 0),
                  width: 32.dp,
                  height: 32.dp,
                  child: InkWell(
                    child: Image.asset(R.imagesChatButtonPaperPlane),
                    onTap: () {
                      _visZhifeiji.value = true;
                      this.getHistoryMessages();
                      _messageText.text = "";
                    },
                  ),
                ),
              )),
            ],
          ),
        ),
        ///表情
        Offstage(
          offstage: !emojiShowing,
          child: SizedBox(
            height: 250.dp,
            width: 1000.dp,
            child: EmojiPicker(
                onEmojiSelected: (Category category, Emoji emoji) {
                  _onEmojiSelected(emoji);
                },
                onBackspacePressed: _onBackspacePressed,
                config: const Config(
                    columns: 7,
                    emojiSizeMax: 25.0,
                    verticalSpacing: 0,
                    horizontalSpacing: 0,
                    initCategory: Category.RECENT,
                    bgColor: Color(0xFFF2F2F2),
                    indicatorColor: Color(0xff65DAC5),
                    iconColor: Colors.orange,
                    iconColorSelected: Color(0xff65DAC5),
                    progressIndicatorColor: Color(0xff65DAC5),
                    backspaceColor: Color(0xff65DAC5),
                    showRecentsTab: true,
                    recentsLimit: 28,
                    noRecentsText: 'No Recents',
                    noRecentsStyle:
                    TextStyle(fontSize: 20, color: Colors.black26),
                    categoryIcons: CategoryIcons(),
                    buttonMode: ButtonMode.MATERIAL)),
          ),
        )
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
        value: SystemUiOverlayStyle.light,
        child: KeyboardVisibilityBuilder( /// 检测键盘是否弹出
          builder: (context, isKeyboardVisible){
            if(isKeyboardVisible){ /// 当键盘弹出时自动跳转到最底部
              scrollController.jumpTo(scrollController.position.maxScrollExtent);
            }
            return GestureDetector(
              onTap: () => {
                hideKeyboard(context), /// 隐藏键盘
                emojiShowing = false
              },
              child: Container(
                decoration: BoxDecoration(
                    image: DecorationImage(
                        image: AssetImage(
                          R.imagesHealingIconBackground,
                        ),
                        fit: BoxFit.fill
                    )
                ),
                child: Stack(
                  fit: StackFit.expand,
                  children: [
                    _controller.value.isInitialized
                        ? Transform.scale(
                      scale: _controller.value.aspectRatio /
                          MediaQuery.of(context).size.aspectRatio,
                      child: Center(
                        child: AspectRatio(
                          aspectRatio: _controller.value.aspectRatio,
                          child: VideoPlayer(_controller),
                        ),
                      ),
                    )
                    : Image.asset(R.imagesHealingIconBackground),
                    Scaffold(
                        backgroundColor: Colors.transparent,
                        body: Container(
                          height: double.infinity,
                          width: double.infinity,
                          child: _chatList(context),
                        )
                    )
                  ],
                ),
              ),
            );
          },
        )
    );
  }
}
  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
Flutter中实现聊天页面底部输入栏键盘与表情页的切换,可以使用软键盘监听和底部弹出框的方式。 首先,你可以使用`keyboard_visibility`库来监听软键盘的可见性。这个库提供了一个`KeyboardVisibility.onChange`回调,可以在软键盘的可见性发生变化时触发。 接下来,你可以使用一个`TextField`作为输入框,并在输入框上方放置一个底部弹出框,用于展示表情页。当软键盘弹出时,隐藏底部弹出框,当软键盘收起时,显示底部弹出框。 下面是一个简单的示例代码: ```dart import 'package:flutter/material.dart'; import 'package:keyboard_visibility/keyboard_visibility.dart'; class ChatPage extends StatefulWidget { @override _ChatPageState createState() => _ChatPageState(); } class _ChatPageState extends State<ChatPage> { bool isKeyboardVisible = false; @override void initState() { super.initState(); KeyboardVisibility.onChange.listen((bool visible) { setState(() { isKeyboardVisible = visible; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Chat'), ), body: Column( children: [ Expanded( child: ListView.builder( itemCount: 10, itemBuilder: (context, index) => ListTile( title: Text('Message $index'), ), ), ), if (!isKeyboardVisible) Container( padding: EdgeInsets.all(8), child: Row( children: [ Expanded( child: TextField( decoration: InputDecoration( hintText: 'Enter a message', ), ), ), IconButton( icon: Icon(Icons.emoji_emotions), onPressed: () { // 弹出表情页 // ... }, ), ], ), ), ], ), ); } } ``` 在上述代码中,我们使用`KeyboardVisibility.onChange`监听软键盘的可见性变化,并在状态发生变化时更新`isKeyboardVisible`的值。通过判断`isKeyboardVisible`的值,决定是否显示底部输入栏。 你可以在`IconButton`的`onPressed`回调中实现弹出表情页的逻辑。根据你的需求,可以使用底部弹出对话框、底部弹出菜单或其他自定义的方式来展示表情页。 希望这个示例能帮到你!如果还有其他问题,请随时提问。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

倾云鹤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值