說來奇怪,即時通訊領域的霸主QQ,微信,旗下產品出的騰訊即時通訊IM就像個殘疾人一樣,這裏不對那裏不對,要達到生產級別,就不得不去改它很多源碼纔行。今天先不吐槽其他的,我們看看如何在騰訊Im裏面完成語音通話
功能。
大致分爲以下幾步:
- 原材料準備
- 初步實現語音通話
- 完善通話邏輯
- 鈴聲震動實現、懸浮窗實現
- 細節優化
原材料準備
- 騰訊最新版實時音視頻SDK(我這裏下載的是精簡版TRTC)
- Android Studio 3.5+(需要升級Android Studio的可以參考一下我寫的
升級Android Studio踩坑)的文章,Android 4.1及以上系統(騰訊要求)
初步實現語音通話(根據騰訊的文檔集成SDK)
1、集成SDK
- 在模塊的build.gradle中的 dependencies中添加
dependencies {
implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'
}
- 在defaultCOnfig中,指定CPU架構
defaultConfig {
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
}
}
- 配置權限
最後,如果這篇對你有一丁點幫助,請點個贊再走吧,謝謝了喂。
<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" />
- 設置混淆
-keep class com.tencent.** { *; }
- 設置打包參數
packagingOptions {
pickFirst '**/libc++_shared.so'
doNotStrip "*/armeabi/libYTCommon.so"
doNotStrip "*/armeabi-v7a/libYTCommon.so"
doNotStrip "*/x86/libYTCommon.so"
doNotStrip "*/arm64-v8a/libYTCommon.so"
}
2、實現通話
- 複製源碼文件夾trtcaudiocalldemo 中的ui和model到項目中。這裏看自己的需求進行選擇,實現語音通話,我們只需要
TRTCAudioCallActivity.java
文件 - 複製
CallService
到項目中,這個Service主要負責處理接聽電話的事務(接聽電話需要進房需要查詢用戶信息,生成一個beingCallUserModel
傳入) - 調用
TRTCAudioCallActivity.startCallSomeone(getContext(), mContactList);
發起語音通話,這裏的mContactList 如果是單聊或者羣聊只邀請一個人,只會有一個model,查詢設置這個model的avatar、phone、userid、username、groupId
即可。到此初步集成完畢,可以進行語音通話了。
完善通話邏輯
1、Android端的通話邏輯並不完善,讓我們來看看它的問題
- 不會發送結束消息,任何情況下的掛斷都是發送 取消命令
- 羣通話遠端用戶離開房間不會觸發通話掛斷
問題所在:TRTCAuduiCallImpl中的hangup 在通話進行中或者發起人主動掛斷的情況下只會發送取消通話命令
騰訊自己也知道自己有問題,留了一個todo。那麼我們如何修改呢?
根據正常的打電話邏輯,A打給B,會有以下幾種情況- 未通話:A取消,B拒絕,
- 通話中:A掛斷 ,B掛斷
首先B拒絕,會在hangup方法中進入reject()方法中,發送一個拒絕的消息,這個我們不用處理;然後是A取消的情況,可以通過判斷邀請列表的人,如果邀請列表的人大於0,這個時候掛斷,那麼一定是A取消;再是A掛斷和B掛斷,這裏得區分一下在羣聊通話,還是單聊通話,如果是單聊通話,那麼A掛斷 就是A判斷房間中用戶數未0,發送一個通話結束消息出去,同理B一樣。如果是羣聊中,那麼就是最後一個退出房間的人判斷,發送一個通話結束的消息出去。
所以在羣聊和單聊中沒我們可以這樣判斷:
Log.d(TAG, "Hangup: " + mCurRoomUserSet + " " + mCurInvitedList + " " + mIsInRoom);
if (mIsInRoom) {
if (isCollectionEmpty(mCurRoomUserSet)) {
if (mCurInvitedList.size() > 0) {
//取消
sendModel("", CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL);
} else {
//通話結束
sendModel("", CallModel.VIDEO_CALL_ACTION_HANGUP);
}
}
}
stopCall();
exitRoom();
}
並且如果是羣聊 ,需要在遠端用戶退出羣主,並且羣主裏面沒有用戶的時候發送通話結束的消息即 在preExitRoom
方法裏面調用groupHangup
方法,並且退房相關操作需要註釋掉,因爲groupHangup
方法裏面會對房間參數進行判斷,需要發消息,然後退房。
當然發送消息並退房並不是所有情況都適用,比如忙線,拒接、超時的時候,就只需要執行退房操作,所以在這些情況下不能調用groupHangup
方法,只判斷執行退房操作。
2、解析自定義消息
這個東西看需求,一般情況下,一次通話都會有兩條消息,即一條發起通話消息,一條結束(拒絕、忙線、掛斷、超時等情況),我這裏貼一下我的解析方式和效果圖:
private void buildVoiceCallView(ICustomMessageViewGroup parent, MessageInfo info, TRTCAudioCallImpl.CallModel data) {
if (data.action == TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_DIALING) {
// 把自定義消息view添加到TUIKit內部的父容器裏
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("您發起了語音通話");
} else {
tv.setText("對方發起了語音通話");
}
return;
}
// 把自定義消息view添加到TUIKit內部的父容器裏
View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_custom_message, null, false);
parent.addMessageContentView(view);
// 自定義消息view的實現,這裏僅僅展示文本信息,並且實現超鏈接跳轉
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 = "通話結束";
} else {
text = "通話結束 " + 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);
}
鈴聲震動實現、懸浮窗實現
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();
/**
* =============響鈴 震動相關
*/
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() {
//鈴聲相關
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(mp -> {
if (mp != null) {
mp.setLooping(true);
mp.start();
}
});
}
/**
* ==============響鈴、震動相關方法========================
*/
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();
// 設置 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);
// 設置此值可在撥打時控制響鈴音量
am.setMode(AudioManager.MODE_IN_COMMUNICATION);
// 設置撥打時響鈴音量默認值
am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, 8, AudioManager.STREAM_VOICE_CALL);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 判斷系統響鈴正東相關設置
* 1、系統靜音 不震動 就兩個都不設置
* 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);
}
/**
* 判斷系統是否設置了 響鈴時振動
*/
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;
}
// 退出此頁面後應設置成正常模式,否則按下音量鍵無法更改其他音頻類型的音量
if (AndroidApplication.getInstance() != null) {
AudioManager am = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
if (am != null) {
am.setMode(AudioManager.MODE_NORMAL);
}
}
}
}
2、懸浮窗 實現
- 申請權限
- 將當前通話Activity移動到後臺執行
- 開啓懸浮窗服務
1)申請權限
@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獲得您的懸浮窗權限,將影響您接聽對方發起的語音通話。").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;
}
}
當然申請懸浮窗全選會有跳轉到設置界面這個過程,所以還需要添加判斷是否具有懸浮窗權限的判斷過程,這裏就留點發揮空間了。
2)將當前通話Activity移動到後臺執行
這個很簡單,就是將Activity的lunchMode改爲SingleInstance模式,然後直接調用moveTaskToBack(true);方法,這裏傳true,表示任何情況下 都會將Acitivty移動到後臺。但是有得必有失,設置爲SingleInstance模式會爲我們帶來一些問題,這些我會在後面說明。
3)綁定懸浮窗服務,開啓懸浮窗
創建一個懸浮窗Service,獲取WindowManager,在windowManager添加一個自定義的懸浮窗View即可,當然要想懸浮窗可以移動,得重寫懸浮窗的,觸摸事件。在懸浮窗裏面註冊一個本地廣播,方便改變通話狀態,記錄通話時間等等。貼一下代碼,需要自取。
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;
/**
* 開始觸控的座標,移動時的座標(相對於屏幕左上角的座標)
*/
private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;
/**
* 開始時的座標和結束時的座標(相對於自身控件的座標)
*/
private int mStartX, mStartY, mStopX, mStopY;
/**
* 判斷懸浮窗口是否移動,這裏做個標記,防止移動後鬆手觸發了點擊事件
*/
private boolean isMove;
/**
* 判斷是否綁定了服務
*/
private boolean isServiceBind;
/**
* 通話狀態
*/
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();
//設置懸浮窗基本參數(位置、寬高等)
initWindow();
//註冊 BroadcastReceiver 監聽情景模式的切換
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);
}
/**
* 設置懸浮框基本參數(位置、寬高等)
*/
private void initWindow() {
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
//設置好懸浮窗的參數
wmParams = getParams();
// 懸浮窗默認顯示以右上角爲起始座標
wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
// 不設置這個彈出框的透明遮罩顯示爲黑色
wmParams.format = PixelFormat.TRANSLUCENT;
//懸浮窗的開始位置,因爲設置的是從右上角開始,所以屏幕左上角是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;
}
//設置可以顯示在狀態欄上
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;
//設置懸浮窗口長寬數據
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
return wmParams;
}
//加載遠端視屏:在這對懸浮窗內內容做操作
private void initFloating() {
//將子View加載進懸浮窗View
//懸浮窗父佈局
mMainView = mFloatingLayout.findViewById(R.id.layout_dial_float);
//加載進懸浮窗的子View,這個VIew來自天轉過來的那個Activity裏面的那個需要加載的View
mAcceptStatus = mFloatingLayout.findViewById(R.id.tv_accept_status);
// View mChildView = renderView.getChildView();
// mMainView.addView(mChildView);//將需要懸浮顯示的Viewadd到mTXCloudVideoView中
//懸浮框觸摸事件,設置懸浮框可拖動
mMainView.setOnTouchListener(this);
//懸浮框點擊事件
mMainView.setOnClickListener(v -> {
//綁定了服務才跳轉,不綁定服務不跳轉
if (!isServiceBind) {
return;
}
//在這裏實現點擊重新回到Activity
//從該service跳轉至該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;
}
//如果是移動事件不觸發OnClick事件,防止移動的時候一放手形成點擊事件
return isMove;
}
}
細節優化
1、SingleInstance的 Home鍵處理
當luncherModel爲SingleInstance的時候,點擊Home鍵會引發很多問題
- 點擊圖標回到app的時候進入到的是第一個棧,而不是打電話頁面
我的解決辦法是在聊天頁面檢測通話頁面是否正在運行,如果在運行的話,生成一個正在進行語音通話的noticeLayout,然後給noticeLauout設置點擊回到語音通話頁面。 - 點擊recent鍵,會回到最初的狀態,即 就算通話已經結束,從recent回去 會變成打電話的初始狀態。
設置一個通話是否結束的標記位,保存在SharePreference裏面,在onCreate 中進行判斷,如果是已經結束的通話,就加載另外一套通話結束的頁面。
2、當應用退到後臺的時候,部分手機無法喚起後臺彈出(小米手機)功能,而有些手機又會直接彈出,顯然這兩種都不友好。
我們在接電話的地方設置一個30s的計時器,在這30s中不停檢測應用是否在前臺運行,並且判斷通話是否結束,如果檢測過程中兩個條件都滿足了,我們就打開通話頁面,然後取消計時。這樣做有兩個好處,一個是,無法喚起後臺彈出的手機,當我們打開app的收,在有效期之內還能接到電話。另外一個是,能後臺自動彈出的手機,不會突兀的響鈴和亂跳轉頁面。
3、離線打電話消息接收問題
騰訊的離線推送沒有統一的處理,這使得我們監聽離線消息變得十分困難,並且有些手機的離線推送甚至不能被檢測到。這個時候我們換一種思路,我們直接在打開app的時候檢測消息列表的歷史消息,獲取最後一條消息,進行語音通話的消息處理,這樣們在接收離線通知的情況下,也能直接打開到通話頁面
最後
使用騰訊IM和騰訊實時音視頻 的坑很多,不過都被我們一一淌過來了,如果你遇到不好解決的問題,歡迎留言交流,最後,如果這篇對你有一丁點幫助,請點個贊再走吧,謝謝。