說來奇怪,即時通訊領(lǐng)域的霸主QQ,微信,旗下產(chǎn)品出的騰訊即時通訊IM就像個殘疾人一樣,這里不對那里不對,要達到生產(chǎn)級別,就不得不去改它很多源碼才行。今天先不吐槽其他的,我們看看如何在騰訊Im里面完成語音通話功能。
大致分為以下幾步:
- 原材料準(zhǔn)備
- 初步實現(xiàn)語音通話
- 完善通話邏輯
- 鈴聲震動實現(xiàn)、懸浮窗實現(xiàn)
- 細節(jié)優(yōu)化
原材料準(zhǔn)備
- 騰訊最新版實時音視頻SDK(我這里下載的是精簡版TRTC)
- Android Studio 3.5+(需要升級Android Studio的可以參考一下我寫的
升級Android Studio踩坑)的文章,Android 4.1及以上系統(tǒng)(騰訊要求)
初步實現(xiàn)語音通話(根據(jù)騰訊的文檔集成SDK)
1、集成SDK
- 在模塊的build.gradle中的 dependencies中添加
dependencies {
implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'
}
- 在defaultCOnfig中,指定CPU架構(gòu)
defaultConfig {
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
}
}
- 配置權(quán)限
最后,如果這篇對你有一丁點幫助,請點個贊再走吧,謝謝了喂。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses- permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
- 設(shè)置混淆
-keep class com.tencent.** { *; }
- 設(shè)置打包參數(shù)
packagingOptions {
pickFirst '**/libc++_shared.so'
doNotStrip "*/armeabi/libYTCommon.so"
doNotStrip "*/armeabi-v7a/libYTCommon.so"
doNotStrip "*/x86/libYTCommon.so"
doNotStrip "*/arm64-v8a/libYTCommon.so"
}
2、實現(xiàn)通話
- 復(fù)制源碼文件夾trtcaudiocalldemo 中的ui和model到項目中。這里看自己的需求進行選擇,實現(xiàn)語音通話,我們只需要
TRTCAudioCallActivity.java文件 - 復(fù)制
CallService到項目中,這個Service主要負責(zé)處理接聽電話的事務(wù)(接聽電話需要進房需要查詢用戶信息,生成一個beingCallUserModel傳入) - 調(diào)用
TRTCAudioCallActivity.startCallSomeone(getContext(), mContactList);發(fā)起語音通話,這里的mContactList 如果是單聊或者群聊只邀請一個人,只會有一個model,查詢設(shè)置這個model的avatar、phone、userid、username、groupId即可。到此初步集成完畢,可以進行語音通話了。
完善通話邏輯
1、Android端的通話邏輯并不完善,讓我們來看看它的問題
不會發(fā)送結(jié)束消息,任何情況下的掛斷都是發(fā)送 取消命令
-
群通話遠端用戶離開房間不會觸發(fā)通話掛斷
問題所在:TRTCAuduiCallImpl中的hangup 在通話進行中或者發(fā)起人主動掛斷的情況下只會發(fā)送取消通話命令
image.png騰訊自己也知道自己有問題,留了一個todo。那么我們?nèi)绾涡薷哪兀?br> 根據(jù)正常的打電話邏輯,A打給B,會有以下幾種情況
- 未通話:A取消,B拒絕,
- 通話中:A掛斷 ,B掛斷
首先B拒絕,會在hangup方法中進入reject()方法中,發(fā)送一個拒絕的消息,這個我們不用處理;然后是A取消的情況,可以通過判斷邀請列表的人,如果邀請列表的人大于0,這個時候掛斷,那么一定是A取消;再是A掛斷和B掛斷,這里得區(qū)分一下在群聊通話,還是單聊通話,如果是單聊通話,那么A掛斷 就是A判斷房間中用戶數(shù)未0,發(fā)送一個通話結(jié)束消息出去,同理B一樣。如果是群聊中,那么就是最后一個退出房間的人判斷,發(fā)送一個通話結(jié)束的消息出去。
所以在群聊和單聊中沒我們可以這樣判斷:
Log.d(TAG, "Hangup: " + mCurRoomUserSet + " " + mCurInvitedList + " " + mIsInRoom);
if (mIsInRoom) {
if (isCollectionEmpty(mCurRoomUserSet)) {
if (mCurInvitedList.size() > 0) {
//取消
sendModel("", CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL);
} else {
//通話結(jié)束
sendModel("", CallModel.VIDEO_CALL_ACTION_HANGUP);
}
}
}
stopCall();
exitRoom();
}
并且如果是群聊 ,需要在遠端用戶退出群主,并且群主里面沒有用戶的時候發(fā)送通話結(jié)束的消息即 在preExitRoom方法里面調(diào)用groupHangup方法,并且退房相關(guān)操作需要注釋掉,因為groupHangup方法里面會對房間參數(shù)進行判斷,需要發(fā)消息,然后退房。
當(dāng)然發(fā)送消息并退房并不是所有情況都適用,比如忙線,拒接、超時的時候,就只需要執(zhí)行退房操作,所以在這些情況下不能調(diào)用groupHangup方法,只判斷執(zhí)行退房操作。
2、解析自定義消息
這個東西看需求,一般情況下,一次通話都會有兩條消息,即一條發(fā)起通話消息,一條結(jié)束(拒絕、忙線、掛斷、超時等情況),我這里貼一下我的解析方式和效果圖:
private void buildVoiceCallView(ICustomMessageViewGroup parent, MessageInfo info, TRTCAudioCallImpl.CallModel data) {
if (data.action == TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_DIALING) {
// 把自定義消息view添加到TUIKit內(nèi)部的父容器里
View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_senc_call_message, null, false);
parent.addMessageItemView(view);
TextView tv = view.findViewById(R.id.tv_content);
if (info.isSelf()) {
tv.setText("您發(fā)起了語音通話");
} else {
tv.setText("對方發(fā)起了語音通話");
}
return;
}
// 把自定義消息view添加到TUIKit內(nèi)部的父容器里
View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_custom_message, null, false);
parent.addMessageContentView(view);
// 自定義消息view的實現(xiàn),這里僅僅展示文本信息,并且實現(xiàn)超鏈接跳轉(zhuǎn)
TextView textView = view.findViewById(R.id.tv_dial_status);
ImageView ivLeft = view.findViewById(R.id.iv_left);
ImageView ivRight = view.findViewById(R.id.iv_right);
if (info.isSelf()) {
ivRight.setVisibility(View.VISIBLE);
ivLeft.setVisibility(View.GONE);
textView.setTextColor(getResources().getColor(R.color.white));
} else {
ivRight.setVisibility(View.GONE);
ivLeft.setVisibility(View.VISIBLE);
textView.setTextColor(getResources().getColor(R.color.color_333333));
}
String text;
switch (data.action) {
case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL:
text = "已取消";
break;
case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_REJECT:
text = "已拒絕";
break;
case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_SPONSOR_TIMEOUT:
text = "無人接聽";
break;
case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_HANGUP:
if (data.duration == 0) {
text = "通話結(jié)束";
} else {
text = "通話結(jié)束 " + TimeUtils.millis2StringByCorrect(data.duration * 1000, data.duration >= 60 * 60 ? "HH:mm:ss" : "mm:ss");
}
break;
case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_LINE_BUSY:
text = "忙線中";
break;
default:
text = "未知通話錯誤";
break;
}
textView.setText(text);
}

鈴聲震動實現(xiàn)、懸浮窗實現(xiàn)
1、鈴聲震動(呼叫和待接聽響鈴,接聽和掛斷停止響鈴)
- 呼叫方 邀請頁面響鈴或震動,在
showInvitingView()方法中添加
//開始呼叫響鈴
if (mRingVibrateHelper != null) { mRingVibrateHelper.initLocalCallRinging();}
- 通話中停止響鈴或震動,在
showCallingView()方法中使用
//停止響鈴if (mRingVibrateHelper != null) { mRingVibrateHelper.stopRing();}
- 接聽方在,接聽等待頁面響鈴或震動,在
showWaitingResponseView()方法中使用
//響鈴或者震動mRingVibrateHelper.initRemoteCallRinging();
- 頁面退出,停止響鈴
if (mRingVibrateHelper != null) {
mRingVibrateHelper.stopRing();
mRingVibrateHelper.releaseMediaPlayer();
}
分享一下響鈴震動幫助類TimRingVibrateHelper
/**
* @author leary
* 響鈴震動幫助類
*/
public class TimRingVibrateHelper {
private static final String TAG = TimRingVibrateHelper.class.getSimpleName();
/**
* =============響鈴 震動相關(guān)
*/
private MediaPlayer mMediaPlayer;
private Vibrator mVibrator;
private static TimRingVibrateHelper instance;
public static TimRingVibrateHelper getInstance() {
if (instance == null) {
synchronized (TimRingVibrateHelper.class) {
if (instance == null) {
instance = new TimRingVibrateHelper();
}
}
}
return instance;
}
private TimRingVibrateHelper() {
//鈴聲相關(guān)
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(mp -> {
if (mp != null) {
mp.setLooping(true);
mp.start();
}
});
}
/**
* ==============響鈴、震動相關(guān)方法========================
*/
public void initLocalCallRinging() {
try {
AssetFileDescriptor assetFileDescriptor = AndroidApplication.getInstance().getResources().openRawResourceFd(R.raw.voip_outgoing_ring);
mMediaPlayer.reset();
mMediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(),
assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength());
assetFileDescriptor.close();
// 設(shè)置 MediaPlayer 播放的聲音用途
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build();
mMediaPlayer.setAudioAttributes(attributes);
} else {
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
}
mMediaPlayer.prepareAsync();
final AudioManager am = (AudioManager) AndroidApplication.getInstance().getSystemService(Context.AUDIO_SERVICE);
if (am != null) {
am.setSpeakerphoneOn(false);
// 設(shè)置此值可在撥打時控制響鈴音量
am.setMode(AudioManager.MODE_IN_COMMUNICATION);
// 設(shè)置撥打時響鈴音量默認值
am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, 8, AudioManager.STREAM_VOICE_CALL);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 判斷系統(tǒng)響鈴正東相關(guān)設(shè)置
* 1、系統(tǒng)靜音 不震動 就兩個都不設(shè)置
* 2、靜音震動
* 3、只響鈴不震動
* 4、響鈴且震動
*/
public void initRemoteCallRinging() {
int ringerMode = getRingerMode(AndroidApplication.getInstance());
if (ringerMode != AudioManager.RINGER_MODE_SILENT) {
if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
startVibrator();
} else {
if (isVibrateWhenRinging()) {
startVibrator();
}
startRing();
}
}
}
private int getRingerMode(Context context) {
AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
return audio.getRingerMode();
}
/**
* 開始響鈴
*/
private void startRing() {
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
try {
mMediaPlayer.setDataSource(AndroidApplication.getInstance(), uri);
mMediaPlayer.prepareAsync();
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "Ringtone not found : " + uri);
try {
uri = RingtoneManager.getValidRingtoneUri(AndroidApplication.getInstance());
mMediaPlayer.setDataSource(AndroidApplication.getInstance(), uri);
mMediaPlayer.prepareAsync();
} catch (Exception e1) {
e1.printStackTrace();
Log.e(TAG, "Ringtone not found: " + uri);
}
}
}
/**
* 開始震動
*/
private void startVibrator() {
if (mVibrator == null) {
mVibrator = (Vibrator) AndroidApplication.getInstance().getSystemService(Context.VIBRATOR_SERVICE);
} else {
mVibrator.cancel();
}
mVibrator.vibrate(new long[]{500, 1000}, 0);
}
/**
* 判斷系統(tǒng)是否設(shè)置了 響鈴時振動
*/
private boolean isVibrateWhenRinging() {
ContentResolver resolver = AndroidApplication.getInstance().getApplicationContext().getContentResolver();
if (Build.MANUFACTURER.equals("Xiaomi")) {
return Settings.System.getInt(resolver, "vibrate_in_normal", 0) == 1;
} else if (Build.MANUFACTURER.equals("smartisan")) {
return Settings.Global.getInt(resolver, "telephony_vibration_enabled", 0) == 1;
} else {
return Settings.System.getInt(resolver, "vibrate_when_ringing", 0) == 1;
}
}
/**
* 停止震動和響鈴
*/
public void stopRing() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
}
if (mVibrator != null) {
mVibrator.cancel();
}
if (AndroidApplication.getInstance() != null) {
//通話時控制音量
AudioManager audioManager = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(AUDIO_SERVICE);
audioManager.setMode(AudioManager.MODE_NORMAL);
}
}
/**
* 釋放資源
*/
public void releaseMediaPlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
}
if (instance != null) {
instance = null;
}
// 退出此頁面后應(yīng)設(shè)置成正常模式,否則按下音量鍵無法更改其他音頻類型的音量
if (AndroidApplication.getInstance() != null) {
AudioManager am = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
if (am != null) {
am.setMode(AudioManager.MODE_NORMAL);
}
}
}
}
2、懸浮窗 實現(xiàn)
- 申請權(quán)限
- 將當(dāng)前通話Activity移動到后臺執(zhí)行
- 開啟懸浮窗服務(wù)
1)申請權(quán)限
@TargetApi(19)
public static boolean canDrawOverlays(final Context context, boolean needOpenPermissionSetting) {
boolean result = true;
if (Build.VERSION.SDK_INT >= 23) {
try {
boolean booleanValue = (Boolean) Settings.class.getDeclaredMethod("canDrawOverlays", Context.class).invoke((Object) null, context);
if (!booleanValue && needOpenPermissionSetting) {
ArrayList<String> permissionList = new ArrayList();
permissionList.add("android.settings.action.MANAGE_OVERLAY_PERMISSION");
showPermissionAlert(context, context.getString(R.string.tim_float_window_not_allowed), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (-1 == which) {
Intent intent = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:" + context.getPackageName()));
context.startActivity(intent);
}
if (-2 == which) {
Toasty.warning(context, "抱歉,您已拒絕DBC獲得您的懸浮窗權(quán)限,將影響您接聽對方發(fā)起的語音通話。").show();
}
}
});
}
Log.i(TAG, "isFloatWindowOpAllowed allowed: " + booleanValue);
return booleanValue;
} catch (Exception var7) {
Log.e(TAG, String.format("getDeclaredMethod:canDrawOverlays! Error:%s, etype:%s", var7.getMessage(), var7.getClass().getCanonicalName()));
return true;
}
} else if (Build.VERSION.SDK_INT < 19) {
return true;
} else {
Object systemService = context.getSystemService(Context.APP_OPS_SERVICE);
Method method;
try {
method = Class.forName("android.app.AppOpsManager").getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);
} catch (NoSuchMethodException var9) {
Log.e(TAG, String.format("NoSuchMethodException method:checkOp! Error:%s", var9.getMessage()));
method = null;
} catch (ClassNotFoundException var10) {
var10.printStackTrace();
method = null;
}
if (method != null) {
try {
Integer tmp = (Integer) method.invoke(systemService, 24, context.getApplicationInfo().uid, context.getPackageName());
result = tmp == 0;
} catch (Exception var8) {
Log.e(TAG, String.format("call checkOp failed: %s etype:%s", var8.getMessage(), var8.getClass().getCanonicalName()));
}
}
Log.i(TAG, "isFloatWindowOpAllowed allowed: " + result);
return result;
}
}
當(dāng)然申請懸浮窗全選會有跳轉(zhuǎn)到設(shè)置界面這個過程,所以還需要添加判斷是否具有懸浮窗權(quán)限的判斷過程,這里就留點發(fā)揮空間了。
2)將當(dāng)前通話Activity移動到后臺執(zhí)行
這個很簡單,就是將Activity的lunchMode改為SingleInstance模式,然后直接調(diào)用moveTaskToBack(true);方法,這里傳true,表示任何情況下 都會將Acitivty移動到后臺。但是有得必有失,設(shè)置為SingleInstance模式會為我們帶來一些問題,這些我會在后面說明。
3)綁定懸浮窗服務(wù),開啟懸浮窗
創(chuàng)建一個懸浮窗Service,獲取WindowManager,在windowManager添加一個自定義的懸浮窗View即可,當(dāng)然要想懸浮窗可以移動,得重寫懸浮窗的,觸摸事件。在懸浮窗里面注冊一個本地廣播,方便改變通話狀態(tài),記錄通話時間等等。貼一下代碼,需要自取。
public class TimFloatWindowService extends Service implements View.OnTouchListener {
private WindowManager mWindowManager;
private WindowManager.LayoutParams wmParams;
private LayoutInflater inflater;
/**
* 浮動布局view
*/
private View mFloatingLayout;
/**
* 容器父布局
*/
private View mMainView;
/**
* 開始觸控的坐標(biāo),移動時的坐標(biāo)(相對于屏幕左上角的坐標(biāo))
*/
private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;
/**
* 開始時的坐標(biāo)和結(jié)束時的坐標(biāo)(相對于自身控件的坐標(biāo))
*/
private int mStartX, mStartY, mStopX, mStopY;
/**
* 判斷懸浮窗口是否移動,這里做個標(biāo)記,防止移動后松手觸發(fā)了點擊事件
*/
private boolean isMove;
/**
* 判斷是否綁定了服務(wù)
*/
private boolean isServiceBind;
/**
* 通話狀態(tài)
*/
private TextView mAcceptStatus;
public class TimBinder extends Binder {
public TimFloatWindowService getService() {
return TimFloatWindowService.this;
}
}
private BroadcastReceiver mTimBroadCastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (isServiceBind && CommonI.TIM.BROADCAST_FLAG_FLOAT_STATUS.equals(intent.getAction())
&& mAcceptStatus != null) {
String status = intent.getStringExtra(CommonI.TIM.KEY_ACCEPT_STATUS);
mAcceptStatus.setText(status);
}
}
};
@Override
public IBinder onBind(Intent intent) {
isServiceBind = true;
initFloating();//懸浮框點擊事件的處理
return new TimBinder();
}
@Override
public void onCreate() {
super.onCreate();
//設(shè)置懸浮窗基本參數(shù)(位置、寬高等)
initWindow();
//注冊 BroadcastReceiver 監(jiān)聽情景模式的切換
IntentFilter filter = new IntentFilter();
filter.addAction(CommonI.TIM.BROADCAST_FLAG_FLOAT_STATUS);
LocalBroadcastManager.getInstance(this).registerReceiver(mTimBroadCastReceiver, filter);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
isServiceBind = false;
if (mFloatingLayout != null) {
// 移除懸浮窗口
mWindowManager.removeView(mFloatingLayout);
mFloatingLayout = null;
}
LocalBroadcastManager.getInstance(this).unregisterReceiver(mTimBroadCastReceiver);
}
/**
* 設(shè)置懸浮框基本參數(shù)(位置、寬高等)
*/
private void initWindow() {
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
//設(shè)置好懸浮窗的參數(shù)
wmParams = getParams();
// 懸浮窗默認顯示以右上角為起始坐標(biāo)
wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
// 不設(shè)置這個彈出框的透明遮罩顯示為黑色
wmParams.format = PixelFormat.TRANSLUCENT;
//懸浮窗的開始位置,因為設(shè)置的是從右上角開始,所以屏幕左上角是x=0;y=0
wmParams.x = 40;
wmParams.y = 160;
//得到容器,通過這個inflater來獲得懸浮窗控件
inflater = LayoutInflater.from(getApplicationContext());
// 獲取浮動窗口視圖所在布局
mFloatingLayout = inflater.inflate(R.layout.layout_tim_float_window, null);
// 添加懸浮窗的視圖
mWindowManager.addView(mFloatingLayout, wmParams);
}
private WindowManager.LayoutParams getParams() {
wmParams = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
//設(shè)置可以顯示在狀態(tài)欄上
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//設(shè)置懸浮窗口長寬數(shù)據(jù)
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
return wmParams;
}
//加載遠端視屏:在這對懸浮窗內(nèi)內(nèi)容做操作
private void initFloating() {
//將子View加載進懸浮窗View
//懸浮窗父布局
mMainView = mFloatingLayout.findViewById(R.id.layout_dial_float);
//加載進懸浮窗的子View,這個VIew來自天轉(zhuǎn)過來的那個Activity里面的那個需要加載的View
mAcceptStatus = mFloatingLayout.findViewById(R.id.tv_accept_status);
// View mChildView = renderView.getChildView();
// mMainView.addView(mChildView);//將需要懸浮顯示的Viewadd到mTXCloudVideoView中
//懸浮框觸摸事件,設(shè)置懸浮框可拖動
mMainView.setOnTouchListener(this);
//懸浮框點擊事件
mMainView.setOnClickListener(v -> {
//綁定了服務(wù)才跳轉(zhuǎn),不綁定服務(wù)不跳轉(zhuǎn)
if (!isServiceBind) {
return;
}
//在這里實現(xiàn)點擊重新回到Activity
//從該service跳轉(zhuǎn)至該activity會將該activity從后臺喚醒,所以activity會走onReStart()
Intent intent = new Intent(TimFloatWindowService.this, TRTCAudioCallActivity.class);
//需要Intent.FLAG_ACTIVITY_NEW_TASK,不然會崩潰
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
});
}
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isMove = false;
mTouchStartX = (int) event.getRawX();
mTouchStartY = (int) event.getRawY();
mStartX = (int) event.getX();
mStartY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
mTouchCurrentX = (int) event.getRawX();
mTouchCurrentY = (int) event.getRawY();
wmParams.x -= mTouchCurrentX - mTouchStartX;
wmParams.y += mTouchCurrentY - mTouchStartY;
Log.i("Tim_FloatingListener", " Cx: " + mTouchCurrentX + " Sx: " + mTouchStartX + " Cy: " + mTouchCurrentY + " Sy: " + mTouchStartY);
if (mFloatingLayout != null) {
mWindowManager.updateViewLayout(mFloatingLayout, wmParams);
}
mTouchStartX = mTouchCurrentX;
mTouchStartY = mTouchCurrentY;
break;
case MotionEvent.ACTION_UP:
mStopX = (int) event.getX();
mStopY = (int) event.getY();
if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
isMove = true;
}
break;
default:
break;
}
//如果是移動事件不觸發(fā)OnClick事件,防止移動的時候一放手形成點擊事件
return isMove;
}
}
細節(jié)優(yōu)化
1、SingleInstance的 Home鍵處理
當(dāng)luncherModel為SingleInstance的時候,點擊Home鍵會引發(fā)很多問題
- 點擊圖標(biāo)回到app的時候進入到的是第一個棧,而不是打電話頁面
我的解決辦法是在聊天頁面檢測通話頁面是否正在運行,如果在運行的話,生成一個正在進行語音通話的noticeLayout,然后給noticeLauout設(shè)置點擊回到語音通話頁面。 - 點擊recent鍵,會回到最初的狀態(tài),即 就算通話已經(jīng)結(jié)束,從recent回去 會變成打電話的初始狀態(tài)。
設(shè)置一個通話是否結(jié)束的標(biāo)記位,保存在SharePreference里面,在onCreate 中進行判斷,如果是已經(jīng)結(jié)束的通話,就加載另外一套通話結(jié)束的頁面。
2、當(dāng)應(yīng)用退到后臺的時候,部分手機無法喚起后臺彈出(小米手機)功能,而有些手機又會直接彈出,顯然這兩種都不友好。
我們在接電話的地方設(shè)置一個30s的計時器,在這30s中不停檢測應(yīng)用是否在前臺運行,并且判斷通話是否結(jié)束,如果檢測過程中兩個條件都滿足了,我們就打開通話頁面,然后取消計時。這樣做有兩個好處,一個是,無法喚起后臺彈出的手機,當(dāng)我們打開app的收,在有效期之內(nèi)還能接到電話。另外一個是,能后臺自動彈出的手機,不會突兀的響鈴和亂跳轉(zhuǎn)頁面。
3、離線打電話消息接收問題
騰訊的離線推送沒有統(tǒng)一的處理,這使得我們監(jiān)聽離線消息變得十分困難,并且有些手機的離線推送甚至不能被檢測到。這個時候我們換一種思路,我們直接在打開app的時候檢測消息列表的歷史消息,獲取最后一條消息,進行語音通話的消息處理,這樣們在接收離線通知的情況下,也能直接打開到通話頁面
最后
使用騰訊IM和騰訊實時音視頻 的坑很多,不過都被我們一一淌過來了,如果你遇到不好解決的問題,歡迎留言交流,最后,如果這篇對你有一丁點幫助,請點個贊再走吧,謝謝。
