IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Flutter聊天页面 -> 正文阅读

[移动开发]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),
                        )
                    )
                  ],
                ),
              ),
            );
          },
        )
    );
  }
}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-24 15:41:26  更:2021-08-24 15:42:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 9:49:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码