一、依赖与预览
- 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),
)
)
],
),
),
);
},
)
);
}
}