FlutterSocket實戰,跟着走下來,保證你也會寫一個簡單的聊天
歡迎加入Flutter技術交流羣:723609732
前言
首先上面的功能特點讀者可以看到了,由於我還沒有解決如何裁剪視頻第一幀
這個問題,所以有點小瑕疵,但是其他功能是可以的。其中包含了:
- 文字聊天
- 圖片發送(查看)
- 選取圖片
- 錄視頻
- 選取視頻
- 發送語音
接下來就步入正題開始從頭給讀者介紹一下整個流程,由於本文章主要介紹Socket方面的功能,所以關於消息列表的代碼我這裏就不放出來了,如果需要的話,請給我留言,其中可能也有我寫的不好的地方,請指正出來,謝謝!
文章觀看提醒:
本文章由於牽扯功能和知識點太多,所以代碼的意思都寫在每一行了,請讀者細心觀看
每一行的註釋,由於考慮到基礎的朋友,所以註釋寫的比較多,請見諒~,如果有什麼問題,請留言並聯系我!
需要用到的插件:
- fluttertoast:輕提示
- flutter_screenutil:屏幕適配
- dio:http請求
- provide:狀態管理
- shared_preferences:本地存儲
- permission_handler:權限請求
- image_picker:選擇圖片
- photo_view:查看圖片
- flutter_ijkplayer:視頻播放
- flutter_easyrefresh:下拉刷新
- flutter_plugin_record:語音錄製播放
- connectivity:網絡狀態管理
一、構思最基礎的聊天頁面
首先我們可以簡單的想象一下微信的聊天流程,通過聊天列表進入到聊天頁面後,無非都是一樣的,每個聊天頁面的功能都是一樣的,但是發送出去的,是顯示的不同的數據
從上面我們可以瞭解到,我們需要一個公用的聊天頁面
,這個聊天頁面就是用來顯示每個人不同的消息列表。
首先我是想自己寫聊天頁面的,但是在研究下拉刷新和上拉加載的時候,發現插件flutter_easyRefresh是自帶聊天頁面的,於是我便將頁面copy
了過來,又在之前只能聊天的基礎上改動了一番。
聊天頁面:ChatPage.dart
首先我要先把引入的這些東西給大家說清楚,不然你們在用的時候很容易懵掉,Flutter自己的我就不說了哈。。。
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart'; // 上傳文件傳輸Form表單時需要用到
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart'; // easyRefresh插件,聊天頁面的基礎以及下拉刷新歷史記錄時會用到
import 'package:flutter_spinkit/flutter_spinkit.dart'; //Loading控件
import 'package:fluttertoast/fluttertoast.dart'; // 輕提示
import 'package:project/config/api.dart'; // 這個是我自己封裝的接口,關於所有的請求都要走這裏
import 'package:project/plugins/Plugins.dart'; //這個是我封裝的公用方法
import 'package:project/plugins/PublicStorage.dart'; //這個是我封裝的公用的本地存儲的方法
import 'package:project/plugins/ScreenAdapter.dart'; //這個是我封裝的公用的屏幕適配方法
import 'package:provide/provide.dart'; // 狀態管理
import 'package:project/provide/SocketProvide.dart'; // socket的狀態管理
import 'weChatRecoding.dart'; // 錄音控件
import 'package:flutter_plugin_record/flutter_plugin_record.dart'; // 錄音插件
關於上面我自己封裝的方法如果各位有什麼疑惑可點擊下方鏈接看一下我以前的文章
屏幕適配以及api接口封裝
本地存儲封裝
ChatPage.dar基礎框架代碼
,將此代碼複製下來後,應該就可以看到一個簡單的聊天界面了,但是隻能發送文字,並不能發送別的,
而且也只是一個沒有socket
的聊天,顯然這並不是我們想要的,先別急,我們的界面已經出來了
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:example/generated/i18n.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
/// 聊天界面示例
class ChatPage extends StatefulWidget {
@override
ChatPageState createState() {
return ChatPageState();
}
}
class ChatPageState extends State<ChatPage> {
// 信息列表
List<MessageEntity> _msgList;
// 輸入框
TextEditingController _textEditingController;
// 滾動控制器
ScrollController _scrollController;
@override
void initState() {
super.initState();
_msgList = [
MessageEntity(true, "It's good!"),
MessageEntity(false, 'EasyRefresh'),
];
_textEditingController = TextEditingController();
_textEditingController.addListener(() {
setState(() {});
});
_scrollController = ScrollController();
}
@override
void dispose() {
super.dispose();
_textEditingController.dispose();
_scrollController.dispose();
}
// 發送消息
void _sendMsg(String msg) {
setState(() {
_msgList.insert(0, MessageEntity(true, msg));
});
_scrollController.animateTo(0.0,
duration: Duration(milliseconds: 300), curve: Curves.linear);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('KnoYo'),
centerTitle: false,
backgroundColor: Colors.grey[200],
elevation: 0.0,
actions: <Widget>[
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
},
),
],
),
backgroundColor: Colors.grey[200],
body: Column(
children: <Widget>[
Divider(
height: 0.5,
),
Expanded(
flex: 1,
child: EasyRefresh.custom(
scrollController: _scrollController,
reverse: true,
footer: CustomFooter(
enableInfiniteLoad: true,
extent: 40.0,
triggerDistance: 50.0,
footerBuilder: (context,
loadState,
pulledExtent,
loadTriggerPullDistance,
loadIndicatorExtent,
axisDirection,
float,
completeDuration,
enableInfiniteLoad,
success,
noMore) {
return Stack(
children: <Widget>[
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
width: 30.0,
height: 30.0,
child: SpinKitCircle(
color: Colors.green,
size: 30.0,
),
),
),
],
);
}),
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildMsg(_msgList[index]);
},
childCount: _msgList.length,
),
),
],
onLoad: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_msgList.addAll([
MessageEntity(true, "It's good!"),
MessageEntity(false, 'EasyRefresh'),
]);
});
}
});
},
),
),
SafeArea(
child: Container(
color: Colors.grey[100],
padding: EdgeInsets.only(
left: 15.0,
right: 15.0,
top: 8.0,
bottom: 8.0,
),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
padding: EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 5.0,
bottom: 5.0,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
child: TextField(
controller: _textEditingController,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: 2.0,
bottom: 2.0,
),
border: InputBorder.none,
),
onSubmitted: (value) {
if (_textEditingController.text.isNotEmpty) {
_sendMsg(_textEditingController.text);
_textEditingController.text = '';
}
},
),
),
),
InkWell(
onTap: () {
if (_textEditingController.text.isNotEmpty) {
_sendMsg(_textEditingController.text);
_textEditingController.text = '';
}
},
child: Container(
height: 30.0,
width: 60.0,
alignment: Alignment.center,
margin: EdgeInsets.only(
left: 15.0,
),
decoration: BoxDecoration(
color: _textEditingController.text.isEmpty
? Colors.grey
: Colors.green,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
child: Text(
S.of(context).send,
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
),
),
],
),
),
),
],
),
);
}
// 構建消息視圖
Widget _buildMsg(MessageEntity entity) {
if (entity == null || entity.own == null) {
return Container();
}
if (entity.own) {
return Container(
margin: EdgeInsets.all(
10.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
S.of(context).me,
style: TextStyle(
color: Colors.grey,
fontSize: 13.0,
),
),
Container(
margin: EdgeInsets.only(
top: 5.0,
),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.lightGreen,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
constraints: BoxConstraints(
maxWidth: 200.0,
),
child: Text(
entity.msg ?? '',
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: 16.0,
),
),
)
],
),
Card(
margin: EdgeInsets.only(
left: 10.0,
),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
elevation: 0.0,
child: Container(
height: 40.0,
width: 40.0,
child: Image.asset('assets/image/head.jpg'),
),
),
],
),
);
} else {
return Container(
margin: EdgeInsets.all(
10.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Card(
margin: EdgeInsets.only(
right: 10.0,
),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
elevation: 0.0,
child: Container(
height: 40.0,
width: 40.0,
child: Image.asset('assets/image/head_knoyo.jpg'),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'KnoYo',
style: TextStyle(
color: Colors.grey,
fontSize: 13.0,
),
),
Container(
margin: EdgeInsets.only(
top: 5.0,
),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
constraints: BoxConstraints(
maxWidth: 200.0,
),
child: Text(
entity.msg ?? '',
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: 16.0,
),
),
)
],
),
],
),
);
}
}
}
/// 信息實體
class MessageEntity {
bool own;
String msg;
MessageEntity(this.own, this.msg);
}
二、創建provide消息實體
關於provide方面的知識我就多介紹了,不清楚的讀者請查看我的這篇文章:Ftter狀態持久化以及狀態管理
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:project/config/socket.dart';
import 'package:project/plugins/PublicStorage.dart';
class SocketProvider with ChangeNotifier{
int a = 1;
Socket socket; // 存儲socket實例
String netWorkstate = 'none'; // 網絡狀態
List<ChatRecord> records = List<ChatRecord>();
// 存儲socket實例
setSocket(val){
this.socket = val;
notifyListeners();
}
// 存儲發送來的消息
// 私聊消息,消息類型, 是不是我發送的,語音時間長度, 是否需要顯示成網絡信息, 是不是歷史記錄存儲
setRecords(message,String type, bool newIsMe,{String time_length, history:false}){
records..insert(0, ChatRecord(message: message,type:type, newIsMe: newIsMe, time_length:time_length));
notifyListeners();
}
// 清空消息頁面
clearRecords(){
records = [];
}
// 設置當前網絡狀態
setnetWorkState(val, context) async {
var socket = new ClientSocket(); // 在這裏實例化socket,這裏是整個APP的socket調用最開始的地方
var token = await PublicStorage.getHistoryList('token'); // 獲取本地token
// 只有當之前沒有網絡的時候,從新有了網絡後並且token存在的情況下才會從新觸發socket通信
// 默認的時候是none 所以初始化第一次打開的時候是可以觸發的
if(token.isNotEmpty && netWorkstate == 'none' && (val == '4G' || val == 'Wifi')){
print('-----------------------------鏈接socket-------------------------');
socket.Connect(context);
}
// 設置網絡狀態
netWorkstate = val;
notifyListeners();
}
// 獲取當前網絡狀態
getnetWorkState(){
return netWorkstate;
}
}
// 私聊發送消息內容數據
class ChatRecord{
var message; // 消息內容
String time_length; // 語音時長
String type; // 消息類型
bool newIsMe; // 是不是我發送的
ChatRecord({this.message, this.type, this.newIsMe, this.time_length});
}
設置網絡狀態頁面:Tab.dart
由於該頁面涉及太多代碼,我只粘貼設置網絡的那段,也就是調用setnetWorkState的那一段,直接在initState中調用即可,_state變量可以寫一下,用來記錄網絡的,雖然用不到。。。
_listenNetWork(){
_subscription = Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) {
if (result == ConnectivityResult.mobile) {
Provide.value<SocketProvider>(context).setnetWorkState('4G', context);
setState(() {
_state = "手機網絡";
});
// I am connected to a mobile network.
} else if (result == ConnectivityResult.wifi) {
Provide.value<SocketProvider>(context).setnetWorkState('Wifi', context);
setState(() {
_state = "Wifi 網絡";
});
} else {
Fluttertoast.showToast(msg: '網絡不穩定~ 請檢查!');
Provide.value<SocketProvider>(context).setnetWorkState('none', context);
setState(() {
_state = "沒有網絡";
});
}
});
}
三、實例化,公用化socket
接下來我們開始寫socket的封裝了,由於聊天功能是需要在一開始啓動APP時,就要連接服務器,所以我們來把socket寫到provide
狀態管理當中,這樣,我們其他的頁面也能通過方法來調用到socket的實例了,還有就是寫socket中需要注意的幾個問題,我先列出來,具體可以看代碼
- App啓動時就要連接socket
- 連接socket前需要判斷本地是否有token以及網絡狀態(由於需要判斷網絡狀態,所以需要安裝個網絡狀態的插件connectivity)
- socket需要接受到服務器發來 數據信息來進行相應的處理
- socket在出現斷開的問題後的應對措施
- socket需要向後臺每隔15秒發送一次心跳,以保持socket的活躍度,發送心跳時,也要監聽網絡狀態以及本地token
主要代碼
:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:project/plugins/Plugins.dart';
import 'package:provide/provide.dart';
import 'package:project/provide/SocketProvide.dart';
import 'package:project/plugins/PublicStorage.dart';
import 'package:provide/provide.dart';
import 'package:project/provide/userinformation.dart';
import 'api.dart';
class ClientSocket {
var gl_sock; // 拿到socket實例,存儲到provide中,便於其他頁面使用socket的方法
var response; // 接收socket返回回來的數據
var gl_context; // 存儲傳入進來的context,provide的方法在調用時需要傳
var token; // 獲取本地token
int commandTime = 15; // 向後臺發送心跳的時間
Timer timer;
bool netWorkStatus = true; // 網絡狀態
bool socketStatus = false; // socket狀態
// 初始化socket連接
void Connect(context) async {
// 獲取本地存儲Token
token = await PublicStorage.getHistoryList('token');
// 判斷token是否爲空並且網絡狀態是否爲沒有網絡:停止心跳發送
if(token.isEmpty || !netWorkStatus){
timer = null;
return;
}
//創建一個Socket連接到指定地址與端口
await Socket.connect('11.111.111.11', 9500).then((socket){
print('---------連接成功------------');
socketStatus = true;
gl_sock = socket;
gl_context = context;
// 存儲全局socket對象
Provide.value<SocketProvider>(context).setSocket(gl_sock);
// 全局的socket設置爲在線狀態,該方法由項目決定酌情添加
// Provide.value<SocketProvider>(context).setOnlineSocket(true);
// 向服務器發送token驗證
Map arguments = {
"type":"verify_token",
"token":token[0]
};
gl_sock.write(json.encode(arguments));
// socket監聽
gl_sock.listen(dataHandler,
// onError: errorHandler,
onDone: doneHandler,
cancelOnError: false);
// gl_sock.close();
}).catchError((e) {
print("socket無法連接: $e");
});
}
// 接收socket返回報文
dataHandler(data) async {
print('-------Socket發送來的消息-------');
var cnData = await utf8.decode(data); // 將信息轉爲正常文字
response = json.decode(cnData.toString());
print(response);
// 判斷返回的狀態信息token驗證是否成功,如果相等,變可以socket通信
if(response['type'] == 'verify_success'){
Fluttertoast.showToast(msg: '歡迎登錄~');
// 給後臺發送心跳
heartbeatSocket();
return;
}
// 判斷 服務器返回的接收人id和發送人id如果不是同一個的話,開始進行socket信息的存儲
// recv_id:接收人、send_id:發送人
if(response['recv_id'] != int.parse(response['send_id'])){
// 判斷消息類型,存儲到provide消息實體當中
switch (response['content_type']) {
case 'text':
Provide.value<SocketProvider>(gl_context).setRecords(response['Content'], 'text', false);
break;
case 'img':
Provide.value<SocketProvider>(gl_context).setRecords(response['Content'],'img', false);
break;
case 'video':
Provide.value<SocketProvider>(gl_context).setRecords(response['Content'],'video', false);
break;
case 'audio':
Provide.value<SocketProvider>(gl_context).setRecords(response['Content'],'audio', false, time_length:response['time_length']);
break;
default:
}
}
}
// Socket出現斷開的問題
void doneHandler(){
socketStatus = false;
Fluttertoast.cancel(); // 清空所有彈窗
reconnectSocket(); //調用重連socket方法
}
// 重新連接socket
void reconnectSocket(){
int count = 0;
const period = const Duration(seconds: 1);
// 定時器
Timer.periodic(period, (timer) {
// 每一次重連之前,都刪除關掉上一個socket
// gl_sock.close();
gl_sock = null;
count++;
if(count >= 3){
print('時間到了!!!開始從連socket');
// 鏈接socket
Connect(gl_context); // 重連
count = 0; // 倒計時設置爲0
timer.cancel(); // 關閉倒計時
timer = null; // 清空倒計時
Fluttertoast.cancel(); // 關閉彈框
}
});
}
// 心跳機,每15秒給後臺發送一次,用來保持連接
void heartbeatSocket(){
const duration = Duration(seconds:1);
var callback = (time) async {
// 如果socket狀態是斷開的,就停止定時器
if(!socketStatus){
time.cancel();
}
var _subscription = Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) {
// print('--------------------當前網絡:$result------------------------');
if (result != ConnectivityResult.mobile && result != ConnectivityResult.wifi) {
print('沒有網絡,停止定時器');
netWorkStatus = false;
time.cancel();
}else{
netWorkStatus = true;
}
});
token = await PublicStorage.getHistoryList('token');
// token爲空,關閉定時器
if(token.isEmpty){
time.cancel();
return;
}
if(commandTime < 1){
print('-----------------發送心跳------------------');
Map arguments = {
"type":"heartbeat",
};
gl_sock.write(json.encode(arguments));
commandTime = 15;
}else{
commandTime--;
}
};
timer = Timer.periodic(duration, callback);
}
}
三、改造chatPage.dart
現在我們的socket已經寫好了,消息實體Provide類我們也已經寫好了,現在就是將我們消息實體Provide裏面存儲的數據
展示到頁面上了
前提聲明:
- 下面代碼中出現的
arguments
大多數是我從上個頁面傳過來的參數,其中包括:用戶頭像,用戶名,用戶id
- 代碼中出現的
Plugins.
的方法,都是我自己封裝的方法,在後面會貼給大家
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:project/config/api.dart';
import 'package:project/plugins/Plugins.dart';
import 'package:project/plugins/PublicStorage.dart';
import 'package:project/plugins/ScreenAdapter.dart';
import 'package:provide/provide.dart';
import 'package:project/provide/SocketProvide.dart';
import 'weChatRecoding.dart';
import 'package:flutter_plugin_record/flutter_plugin_record.dart';
/// 聊天界面
class ChatPage extends StatefulWidget {
final arguments;
ChatPage(this.arguments);
@override
ChatPageState createState() {
return ChatPageState(this.arguments);
}
}
class ChatPageState extends State<ChatPage> {
// 這裏注意一下這個arguments,是我們用來接收上個頁面傳來的用戶名以及頭像
final arguments;
ChatPageState(this.arguments);
var base_url = 'https://flutter.ikuer.cn';
var api = new Api();
var userInfo;
bool soundRecording = false; // 是否顯示語音按鈕
double _height = 0; // 控制操作欄的高度
var gl_socket;
var localImage, localVideo; // 本地圖片和視頻
var audioPath; // 語音路徑
var duration = Duration(milliseconds: 200);
// 輸入框
TextEditingController _textEditingController;
// 滾動控制器
ScrollController _scrollController;
// 語音播放控制器
FlutterPluginRecord recordPlugin;
// ClientSocket socket;
@override
void initState() {
super.initState();
_setUserData();
_textEditingController = TextEditingController();
_scrollController = ScrollController();
recordPlugin = FlutterPluginRecord();
}
@override
void dispose() {
_textEditingController.dispose();
_scrollController.dispose();
recordPlugin.dispose();
super.dispose();
}
// 獲取用戶數據(這個方法讀者可以酌情處理需不需要,可以全局搜索一下userInfo這個變量的用到的地方再決定)
_setUserData(){
api.getData(context, 'userInfo').then((val){
if(val == null){
return;
}
var response = json.decode(val.toString());
print('--------------獲取服務器用戶數據----------------');
print(response);
setState(() {
userInfo = response;
});
PublicStorage.setHistoryList('UserInfo', response);
});
}
// 發送消息
void _sendMsg(String msg) {
// 存儲文字消息
Provide.value<SocketProvider>(context).setRecords(msg,'text', true);
_scrollController.animateTo(0.0, duration: Duration(milliseconds: 300), curve: Curves.linear);
}
startRecord(){
print("111開始錄製");
}
stopRecord(String path,double audioTimeLength ) async {
print("結束束錄製");
print("音頻文件位置"+path);
print("音頻錄製時長"+audioTimeLength.toString());
setState(() {
this.audioPath = path;
});
api.postData(context, 'uploadFile', formData: await FormData1(path)).then((data){
var audiourl = json.decode(data.toString())['file_path'];
Map contentArguments = {
'type':'private_chat',
'content_type':'audio',
'Content':audiourl,
'time_length':audioTimeLength.toString(),
'recv_id':arguments['recv_id']
};
print(contentArguments);
// socket發送消息
gl_socket.write(json.encode(contentArguments));
// 存儲語音文件
Provide.value<SocketProvider>(context).setRecords(path,'audio', true, time_length:audioTimeLength.toString());
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(icon: Icon(IconData(0xe622, fontFamily: 'myIcon'), size: ScreenAdapter.size(40),), onPressed: ()=>Navigator.pop(context),),
title: Container(
child: Text('${arguments['nickname']}', style: TextStyle(fontSize: ScreenAdapter.size(35)),),
),
centerTitle: true,
// backgroundColor: Colors.grey[200],
elevation: 0.0,
actions: <Widget>[
IconButton(
icon: Icon(Icons.people),
onPressed: (){
print('更多');
},
)
],
),
backgroundColor: Colors.grey[200],
body: Provide<SocketProvider>(
builder: (context, child, val){
return Container(
color: Colors.white,
child: Column(
children: <Widget>[
Divider(
height: 0.5,
),
Expanded(
flex: 1,
child: InkWell(
child: EasyRefresh.custom(
scrollController: _scrollController,
reverse: true,
footer: CustomFooter(
enableInfiniteLoad: false,
extent: 40.0,
triggerDistance: 50.0,
footerBuilder: (context,
loadState,
pulledExtent,
loadTriggerPullDistance,
loadIndicatorExtent,
axisDirection,
float,
completeDuration,
enableInfiniteLoad,
success,
noMore) {
return Stack(
children: <Widget>[
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
width: 30.0,
height: 30.0,
child: SpinKitCircle(
color: Colors.green,
size: 30.0,
),
),
),
],
);
}),
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildMsg(val.records[index]);
},
childCount: val.records.length,
),
),
],
onLoad: () async {
},
),
onTap: (){
setState(() {
_height = 0;
});
},
)
),
SafeArea(
child: Container(
color: Colors.grey[100],
padding: EdgeInsets.only(
left: 15.0,
right: 15.0,
top: 8.0,
bottom: 8.0,
),
child: Row(
children: <Widget>[
InkWell(
child: Container(
margin: EdgeInsets.only(right: ScreenAdapter.setWidth(10)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
),
child: Icon(Icons.keyboard_voice, color: Colors.grey,),
),
onTap: (){
setState(() {
this._height = 0;
this.gl_socket = val.socket;
this.soundRecording = !this.soundRecording;
});
},
),
soundRecording ?
Expanded(
flex: 1,
child: VoiceWidget(startRecord: startRecord,stopRecord: stopRecord),
)
:
Expanded(
flex: 1,
child: Container(
padding: EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 5.0,
bottom: 5.0,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(
20,
)),
),
child: TextField(
controller: _textEditingController,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: 2.0,
bottom: 2.0,
),
border: InputBorder.none,
),
onChanged: (val){
},
onSubmitted: (value) {
if (_textEditingController.text.isNotEmpty) {
_sendMsg(_textEditingController.text);
_textEditingController.text = '';
}
},
onTap: (){
setState(() {
_height = 0;
});
},
),
),
),
InkWell(
child: Container(
margin: EdgeInsets.symmetric(horizontal: ScreenAdapter.setWidth(15)),
decoration: BoxDecoration(
// color: Colors.grey,
borderRadius: BorderRadius.circular(50),
border: Border.all(width: ScreenAdapter.setWidth(0.5), color: Colors.grey)
),
child: Icon(Icons.add, color: Colors.grey,),
),
onTap: (){
// 收起鍵盤
FocusScope.of(context).requestFocus(FocusNode());
setState(() {
this.soundRecording = false;
_height = 100;
});
},
),
// :
InkWell(
onTap: () {
if(_textEditingController.text.isEmpty){
return;
}
Map contentArguments = {
'type':'private_chat',
'content_type':'text',
'Content':_textEditingController.text,
'recv_id':arguments['recv_id']
};
print(contentArguments);
if (_textEditingController.text.isNotEmpty) {
_sendMsg(_textEditingController.text);
_textEditingController.text = '';
}
// socket發送消息
val.socket.write(json.encode(contentArguments));
},
child: Container(
height: 30.0,
width: 60.0,
alignment: Alignment.center,
// margin: EdgeInsets.only(
// left: ScreenAdapter.setWidth(10),
// ),
decoration: BoxDecoration(
// color: _textEditingController.text.isEmpty
// ? Colors.grey
// : Colors.green,
color: Color(0xff4ADDFE),
borderRadius: BorderRadius.all(Radius.circular(
20,
)),
),
child: Text(
'發送',
// S.of(context).send,
style: TextStyle(
color: Colors.white,
fontSize: ScreenAdapter.size(30)
),
),
),
),
],
),
),
),
AnimatedContainer(
duration: duration,
height: _height,
width: MediaQuery.of(context).size.width,
color: Colors.white,
child: Container(
child: GridView(
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 1.0 //寬高比爲2
),
children: <Widget>[
IconButton(
icon: Icon(Icons.camera_alt),
onPressed: () async {
var img_url = await Plugins.takePhoto();
if(img_url!=null){
setState(() {
localImage = img_url;
});
api.postData(context, 'uploadFile', formData: await FormData1(img_url.path)).then((data){
var imgurl = json.decode(data.toString())['file_path'];
Map contentArguments = {
'type':'private_chat',
'content_type':'img',
'Content':imgurl,
'recv_id':arguments['recv_id']
};
print(contentArguments);
Provide.value<SocketProvider>(context).setRecords(localImage,'img', true);
// socket發送消息
val.socket.write(json.encode(contentArguments));
});
}
},
),
IconButton(
icon: Icon(Icons.photo),
onPressed: () async {
print('---------------------選擇圖片---------------------');
var img_url = await Plugins.openGallery();
if(img_url!=null){
setState(() {
localImage = img_url;
});
print(localImage);
api.postData(context, 'uploadFile', formData: await FormData1(img_url.path)).then((data){
var imgurl = json.decode(data.toString())['file_path'];
print('路徑');
print(imgurl);
Map contentArguments = {
'type':'private_chat',
'content_type':'img',
'Content':imgurl,
'recv_id':arguments['recv_id']
};
print(json.encode(contentArguments));
Provide.value<SocketProvider>(context).setRecords(localImage,'img', true);
// // socket發送消息
val.socket.write(json.encode(contentArguments));
});
}
},
),
IconButton(
icon: Icon(Icons.videocam),
onPressed: () async {
print('錄像');
var video_url = await Plugins.takeVideo();
if(video_url!=null){
setState(() {
localVideo = video_url;
});
api.postData(context, 'uploadFile', formData: await FormData1(video_url.path)).then((data){
var videourl = json.decode(data.toString())['file_path'];
Map contentArguments = {
'type':'private_chat',
'content_type':'video',
'Content':videourl,
'recv_id':arguments['recv_id']
};
print(contentArguments);
Provide.value<SocketProvider>(context).setRecords(localVideo.path,'video', true);
// socket發送消息
val.socket.write(json.encode(contentArguments));
});
}
},
),
IconButton(
icon: Icon(Icons.movie),
onPressed: () async {
print('發送視頻');
var video_url = await Plugins.getVideo();
// print(video_url);
if(video_url!=null){
setState(() {
localVideo = video_url;
});
api.postData(context, 'uploadFile', formData: await FormData1(video_url.path)).then((data){
var videourl = json.decode(data.toString())['file_path'];
Map contentArguments = {
'type':'private_chat',
'content_type':'video',
'Content':videourl,
'recv_id':arguments['recv_id']
};
print(contentArguments);
Provide.value<SocketProvider>(context).setRecords(localVideo.path,'video', true);
// socket發送消息
val.socket.write(json.encode(contentArguments));
});
}
},
),
],
)
)
),
],
)
,
);
},
)
);
}
// 構建消息視圖
Widget _buildMsg(ChatRecord entity) {
if (entity == null || entity.newIsMe == null || userInfo == null) {
return Container();
}
if (entity.newIsMe) {
return Container(
margin: EdgeInsets.all(
10.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
margin: EdgeInsets.only(
top: 5.0,
),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: entity.type == 'text' || entity.type == 'audio' ? Color(0xff4ADDFE) : null,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
constraints: BoxConstraints(
maxWidth: 300.0,
maxHeight: 300.0
),
child: MessageWidget(entity)
)
],
),
Card(
margin: EdgeInsets.only(
left: 10.0,
),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
elevation: 0.0,
child: Container(
height: 40.0,
width: 40.0,
child: Image.network('$base_url${userInfo['head_pic']}', fit: BoxFit.cover,),
),
),
],
),
);
} else {
return Container(
margin: EdgeInsets.all(
10.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Card(
margin: EdgeInsets.only(
right: 10.0,
),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
elevation: 0.0,
child: Container(
height: 40.0,
width: 40.0,
child: Image.network('$base_url${arguments['head_pic']}', fit: BoxFit.cover,),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Text(
// '${arguments['nickname']}',
// style: TextStyle(
// color: Colors.grey,
// fontSize: 13.0,
// ),
// ),
Container(
margin: EdgeInsets.only(
top: 5.0,
),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: entity.type == 'text' || entity.type == 'audio' ? Colors.black12 : null,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
constraints: BoxConstraints(
maxWidth: 300.0,
),
child: MessageWidget(entity)
)
],
),
],
),
);
}
}
// 判斷顯示什麼類型的消息
Widget MessageWidget(entity){
switch (entity.type) {
case 'text':
return Text(
entity.message ?? '',
overflow: TextOverflow.clip,
style: TextStyle(
color: entity.newIsMe ? Colors.white : Colors.black,
fontSize: 16.0,
),
);
break;
case 'img':
return entity.newIsMe ?
InkWell(
child: Image.file(entity.message),
onTap: (){
print('點了圖片');
Map arguments = {
'imageProvider':FileImage(entity.message),
'heroTag':'simple'
};
Navigator.pushNamed(context, '/image', arguments: arguments);
},
)
:
InkWell(
child: Image.network('$base_url${entity.message}'),
onTap: (){
Map arguments = {
'imageProvider':NetworkImage('$base_url${entity.message}'),
'heroTag':'simple'
};
Navigator.pushNamed(context, '/image', arguments: arguments);
},
);
break;
case 'video':
return InkWell(
child: Container(
width: ScreenAdapter.setWidth(300),
height: ScreenAdapter.setHeight(150),
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(10)
),
alignment: Alignment.center,
child: Icon(Icons.play_arrow, color: Colors.white,),
),
onTap: (){
Map arguments = {
'isMe':entity.newIsMe,
'message':entity.message
};
Navigator.pushNamed(context, '/video', arguments: arguments);
},
);
break;
case 'audio':
return Container(
width: ScreenAdapter.setWidth(160),
child: entity.newIsMe ?
InkWell(
onTap: (){
playByPath(entity.message);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('${entity.time_length}'),
Container(
margin: EdgeInsets.only(left: ScreenAdapter.setWidth(10)),
width: ScreenAdapter.setWidth(50),
height: ScreenAdapter.setHeight(30),
// color: Colors.red,
child: Image.asset('assets/images/recording_right.png', fit: BoxFit.cover,),
)
],
),
)
:
InkWell(
onTap: (){
// socket發送來的語音
playByPath('$base_url${entity.message}');
},
child: Row(
children: <Widget>[
Text('${entity.time_length}'),
Container(
margin: EdgeInsets.only(left: ScreenAdapter.setWidth(10)),
width: ScreenAdapter.setWidth(50),
height: ScreenAdapter.setHeight(30),
// color: Colors.red,
child: Image.asset('assets/images/recording_left.png', fit: BoxFit.cover,),
)
],
),
)
);
break;
default:
}
}
///播放指定路徑錄音文件
void playByPath(String path) {
recordPlugin.playByPath(path);
}
// dio上傳文件FormData格式
Future<FormData> FormData1(fileUrl) async {
return FormData.fromMap({
"file": await MultipartFile.fromFile(fileUrl)
});
}
}
weChatRecoding.dart:語音發送懸浮窗
import 'package:flutter/material.dart';
import 'package:flutter_plugin_record/flutter_plugin_record.dart';
import 'package:project/plugins/ScreenAdapter.dart';
typedef startRecord = Future Function();
typedef stopRecord = Future Function();
class VoiceWidget extends StatefulWidget {
final Function startRecord;
final Function stopRecord;
/// startRecord 開始錄製回調 stopRecord回調
const VoiceWidget({Key key, this.startRecord, this.stopRecord})
: super(key: key);
@override
_VoiceWidgetState createState() => _VoiceWidgetState();
}
class _VoiceWidgetState extends State<VoiceWidget> {
double starty = 0.0;
double offset = 0.0;
bool isUp = false;
String textShow = "按住說話";
String toastShow = "手指上滑,取消發送";
String voiceIco = "images/voice_volume_1.png";
///默認隱藏狀態
bool voiceState = true;
OverlayEntry overlayEntry;
FlutterPluginRecord recordPlugin;
@override
void initState() {
super.initState();
recordPlugin = new FlutterPluginRecord();
_init();
///初始化方法的監聽
recordPlugin.responseFromInit.listen((data) {
if (data) {
print("初始化成功");
} else {
print("初始化失敗");
}
});
/// 開始錄製或結束錄製的監聽
recordPlugin.response.listen((data) {
if (data.msg == "onStop") {
///結束錄製時會返回錄製文件的地址方便上傳服務器
print("onStop " + data.path);
widget.stopRecord(data.path, data.audioTimeLength);
} else if (data.msg == "onStart") {
print("onStart --");
widget.startRecord();
}
});
///錄製過程監聽錄製的聲音的大小 方便做語音動畫顯示圖片的樣式
recordPlugin.responseFromAmplitude.listen((data) {
var voiceData = double.parse(data.msg);
setState(() {
if (voiceData > 0 && voiceData < 0.1) {
voiceIco = "images/voice_volume_2.png";
} else if (voiceData > 0.2 && voiceData < 0.3) {
voiceIco = "images/voice_volume_3.png";
} else if (voiceData > 0.3 && voiceData < 0.4) {
voiceIco = "images/voice_volume_4.png";
} else if (voiceData > 0.4 && voiceData < 0.5) {
voiceIco = "images/voice_volume_5.png";
} else if (voiceData > 0.5 && voiceData < 0.6) {
voiceIco = "images/voice_volume_6.png";
} else if (voiceData > 0.6 && voiceData < 0.7) {
voiceIco = "images/voice_volume_7.png";
} else if (voiceData > 0.7 && voiceData < 1) {
voiceIco = "images/voice_volume_7.png";
} else {
voiceIco = "images/voice_volume_1.png";
}
if (overlayEntry != null) {
overlayEntry.markNeedsBuild();
}
});
print("振幅大小 " + voiceData.toString() + " " + voiceIco);
});
}
///顯示錄音懸浮佈局
buildOverLayView(BuildContext context) {
if (overlayEntry == null) {
overlayEntry = new OverlayEntry(builder: (content) {
return Positioned(
top: MediaQuery.of(context).size.height * 0.5 - 80,
left: MediaQuery.of(context).size.width * 0.5 - 80,
child: Material(
type: MaterialType.transparency,
child: Center(
child: Opacity(
opacity: 0.8,
child: Container(
width: 160,
height: 160,
decoration: BoxDecoration(
color: Color(0xff77797A),
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 10),
child: new Image.asset(
voiceIco,
width: 100,
height: 100,
package: 'flutter_plugin_record',
),
),
Container(
// padding: EdgeInsets.only(right: 20, left: 20, top: 0),
child: Text(
toastShow,
style: TextStyle(
fontStyle: FontStyle.normal,
color: Colors.white,
fontSize: 14,
),
),
)
],
),
),
),
),
),
);
});
Overlay.of(context).insert(overlayEntry);
}
}
showVoiceView() {
setState(() {
textShow = "鬆開結束";
voiceState = false;
});
buildOverLayView(context);
start();
}
hideVoiceView() {
setState(() {
textShow = "按住說話";
voiceState = true;
});
stop();
if (overlayEntry != null) {
overlayEntry.remove();
overlayEntry = null;
}
if (isUp) {
print("取消發送");
} else {
print("進行發送");
}
}
moveVoiceView() {
// print(offset - start);
setState(() {
isUp = starty - offset > 100 ? true : false;
if (isUp) {
textShow = "鬆開手指,取消發送";
toastShow = textShow;
} else {
textShow = "鬆開結束";
toastShow = "手指上滑,取消發送";
}
});
}
///初始化語音錄製的方法
void _init() async {
recordPlugin.init();
}
///開始語音錄製的方法
void start() async {
recordPlugin.start();
}
///停止語音錄製的方法
void stop() {
recordPlugin.stop();
}
@override
Widget build(BuildContext context) {
return Container(
child: GestureDetector(
onVerticalDragStart: (details) {
starty = details.globalPosition.dy;
showVoiceView();
},
onVerticalDragEnd: (details) {
hideVoiceView();
},
onVerticalDragUpdate: (details) {
offset = details.globalPosition.dy;
moveVoiceView();
},
child: Container(
height: ScreenAdapter.setHeight(50),
decoration: BoxDecoration(
border: Border.all(width: 0.5, color: Colors.grey)
),
child: Center(
child: Text(
textShow,
style: TextStyle(fontSize: ScreenAdapter.size(30)),
),
),
),
),
);
}
@override
void dispose() {
if (recordPlugin != null) {
recordPlugin.dispose();
}
super.dispose();
}
}
Plugins.dart
import 'package:permission_handler/permission_handler.dart'; // 權限申請
import 'package:image_picker/image_picker.dart'; // 選擇圖片
class Plugins{
/*拍照*/
static takePhoto() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera);
// print('拍照返回:' + image.toString());
return image;
}
/*相冊*/
static openGallery() async {
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
// print('相冊返回:' + image.toString());
return image;
}
/*選取視頻*/
static getVideo() async {
var video = await ImagePicker.pickVideo(source: ImageSource.gallery);
return video;
}
/*拍攝視頻*/
static takeVideo() async {
var video = await ImagePicker.pickVideo(source: ImageSource.camera);
// print('拍攝視頻:' + image.toString());
return video;
}
}
showImag.dart圖片
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
class PhotoViewSimpleScreen extends StatelessWidget{
// const PhotoViewSimpleScreen({
// this.aruments;
// this.imageProvider,//圖片
// this.loadingChild,//加載時的widget
// this.backgroundDecoration,//背景修飾
// this.minScale,//最大縮放倍數
// this.maxScale,//最小縮放倍數
// this.heroTag,//hero動畫tagid
// });
Map arguments;
PhotoViewSimpleScreen(
this.arguments
);
// final ImageProvider imageProvider;
// final Widget loadingChild;
// final Decoration backgroundDecoration;
// final dynamic minScale;
// final dynamic maxScale;
// final String heroTag;
@override
Widget build(BuildContext context) {
return Scaffold(
body: InkWell(
onTap: (){
Navigator.pop(context);
},
child: Container(
constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height,
),
child: Stack(
children: <Widget>[
Positioned(
top: 0,
left: 0,
bottom: 0,
right: 0,
child: PhotoView(
imageProvider: arguments['imageProvider'],
// loadingChild: loadingChild,
// backgroundDecoration: backgroundDecoration,
// minScale: true,
// maxScale: 1,
heroAttributes: PhotoViewHeroAttributes(tag: arguments['heroTag']),
enableRotation: false, // 禁止旋轉
),
),
// Positioned(//右上角關閉按鈕
// right: 10,
// top: MediaQuery.of(context).padding.top,
// child: IconButton(
// icon: Icon(Icons.close,size: 30,color: Colors.white,),
// onPressed: (){
// Navigator.of(context).pop();
// },
// ),
// )
],
),
),
)
);
}
}
Video.dart展示視頻
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:project/config/api_url.dart';
import 'package:project/plugins/ScreenAdapter.dart';
import 'package:flutter/material.dart';
class VideoApp extends StatefulWidget {
var arguments;
VideoApp(this.arguments);
@override
_VideoAppState createState() => _VideoAppState(this.arguments);
}
class _VideoAppState extends State<VideoApp> {
var arguments;
_VideoAppState(this.arguments);
// // var aspet = 16/9;
// VideoPlayerController _videoPlayerController;
// ChewieController _chewieController;
@override
void initState() {
super.initState();
print('傳進來的url:${arguments['message']}');
_initVideo();
}
@override
void dispose() async {
// 停止
// 這裏要說明,ijkplayer的stop會釋放資源,導致play不能使用,需要重新準備資源,所以這裏其實採用的是回到進度條開始,並暫停
await controller.stop();
controller.dispose();
super.dispose();
}
_initVideo() async {
if(arguments['isMe']){
await controller.setNetworkDataSource(
arguments['message'],
autoPlay: true
);
}else{
await controller.setNetworkDataSource(
'$base_url${arguments['message']}',
autoPlay: true
);
}
}
IjkMediaController controller = IjkMediaController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
centerTitle: false,
leading: IconButton(
icon: // Icon(IconData(0xe622, fontFamily: 'myIcon'),
Icon(Icons.clear, color: Colors.white, size: ScreenAdapter.size(40),),
onPressed: ()=>Navigator.pop(context),
),
),
body: ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Container(
color: Colors.black,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
// padding: EdgeInsets.all(0),
children: <Widget>[
AspectRatio(
aspectRatio: 1,
child: IjkPlayer(
mediaController: controller,
),
)
]
),
),
),
);
}
}