GitHub地址
主介面(MainActivity)
在主介面,我們需要檢查先Camera和Audio許可權,以適配Andriod6.0及以上版本。
private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 0;
private static final int PERMISSION_REQ_ID_CAMERA = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//檢查Audio許可權
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
//檢查Camera許可權
checkSelfPermission(Manifest.permission.CAMERA, PERMISSION_REQ_ID_CAMERA);
}
}
public boolean checkSelfPermission(String permission, int requestCode) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
return false;
}
return true;
}複製程式碼
頻道介面 (ChannelActivity)
點選開PA!
,進入頻道選擇介面
建立頻道列表
這裡使用RecyclerView建立頻道列表。
/**
* 初始化頻道列表
*/
private void initRecyclerView() {
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new ChannelAdapter(this, mockChannelList()));
}複製程式碼
前置攝像頭預覽
頻道介面背景為前置攝像頭預覽,這個可以使用Android SDK自己實現。但Agora SDK提供了相關API可以直接實現前置攝像頭預覽的功能。具體實現如下:
1. 初始化RtcEngine
RtcEngine是Agora SDK的核心類,叔用一個管理類AgoraManager進行了簡單的封裝,提供操作RtcEngine的核心功能。
初始化如下:
/**
* 初始化RtcEngine
*/
public void init(Context context) {
//建立RtcEngine物件, mRtcEventHandler為RtcEngine的回撥
mRtcEngine = RtcEngine.create(context, context.getString(R.string.private_app_id), mRtcEventHandler);
//開啟視訊功能
mRtcEngine.enableVideo();
//視訊配置,設定為360P
mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION);//設定為通訊模式(預設)
//mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);設定為直播模式
//mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_GAME);設定為遊戲模式
}
/**
* 在Application類中初始化RtcEngine,注意在AndroidManifest.xml中配置下Application
*/
public class LaoTieApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
AgoraManager.getInstance().init(getApplicationContext());
}
}複製程式碼
2. 設定本地視訊
/**
* 設定本地視訊,即前置攝像頭預覽
*/
public AgoraManager setupLocalVideo(Context context) {
//建立一個SurfaceView用作視訊預覽
SurfaceView surfaceView = RtcEngine.CreateRendererView(context);
//將SurfaceView儲存起來在SparseArray中,後續會將其加入介面。key為視訊的使用者id,這裡是本地視訊, 預設id是0
mSurfaceViews.put(mLocalUid, surfaceView);
//設定本地視訊,渲染模式選擇VideoCanvas.RENDER_MODE_HIDDEN,如果選其他模式會出現視訊不會填充滿整個SurfaceView的情況,
//具體渲染模式的區別是什麼,官方也沒有詳細的說明
mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, mLocalUid));
return this;//返回AgoraManager以作鏈式呼叫
}複製程式碼
3. 新增SurfaceView到佈局
@Override
protected void onResume() {
super.onResume();
//先清空容器
mFrameLayout.removeAllViews();
//設定本地前置攝像頭預覽並啟動
AgoraManager.getInstance().setupLocalVideo(getApplicationContext()).startPreview();
//將本地攝像頭預覽的SurfaceView新增到容器中
mFrameLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());
}複製程式碼
4. 停止預覽
/**
* 停止預覽
*/
@Override
protected void onPause() {
super.onPause();
AgoraManager.getInstance().stopPreview();
}複製程式碼
聊天室 (PartyRoomActivity)
點選頻道列表中的選項,跳轉到聊天室介面。聊天室介面顯示規則是:1個人是全屏,2個人是2分屏,3-4個人是4分屏,5-6個人是6分屏, 4分屏和6分屏模式下,雙擊一個小窗,窗會變大,其餘小窗在底部排列。最多支援六人同時聊天。基於這種需求,叔決定寫一個自定義控制元件PartyRoomLayout來完成。PartyRoomLayout直接繼承ViewGroup,根據不同的顯示模式來完成孩子的測量和佈局。
1人全屏
1人全屏其實就是前置攝像頭預覽效果。
前置攝像頭預覽
//設定前置攝像頭預覽並開啟
AgoraManager.getInstance()
.setupLocalVideo(getApplicationContext())
.startPreview();
//將攝像頭預覽的SurfaceView加入PartyRoomLayout
mPartyRoomLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());複製程式碼
PartyRoomLayout處理1人全屏
/**
* 測量一個孩子的情況,孩子的寬高和父容器即PartyRoomLayout一樣
*/
private void measureOneChild(int widthMeasureSpec, int heightMeasureSpec) {
View child = getChildAt(0);
child.measure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 佈局一個孩子的情況
*/
private void layoutOneChild() {
View child = getChildAt(0);
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}複製程式碼
加入頻道
從頻道列表跳轉過來後,需要加入到使用者所選的頻道。
//更新頻道的TextView
mChannel = (TextView) findViewById(R.id.channel);
String channel = getIntent().getStringExtra(“Channel”);
mChannel.setText(channel);
//在AgoraManager中封裝了加入頻道的API
AgoraManager.getInstance()
.setupLocalVideo(getApplicationContext())
.joinChannel(channel)//加入頻道
.startPreview();複製程式碼
結束通話
當使用者點選結束通話按鈕可以退出頻道
mEndCall = (ImageButton) findViewById(R.id.end_call);
mEndCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//AgoraManager裡面封裝了結束通話的API, 退出頻道
AgoraManager.getInstance().leaveChannel();
finish();
}
});複製程式碼
二分屏
事件監聽器
IRtcEngineEventHandler類裡面封裝了Agora SDK裡面的很多事件回撥,在AgoraManager中我們建立了IRtcEngineEventHandler的一個物件mRtcEventHandler,並在建立RtcEngine時傳入。
private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
/**
* 當獲取使用者uid的遠端視訊的回撥
*/
@Override
public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
if (mOnPartyListener != null) {
mOnPartyListener.onGetRemoteVideo(uid);
}
}
/**
* 加入頻道成功的回撥
*/
@Override
public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
if (mOnPartyListener != null) {
mOnPartyListener.onJoinChannelSuccess(channel, uid);
}
}
/**
* 退出頻道
*/
@Override
public void onLeaveChannel(RtcStats stats) {
if (mOnPartyListener != null) {
mOnPartyListener.onLeaveChannelSuccess();
}
}
/**
* 使用者uid離線時的回撥
*/
@Override
public void onUserOffline(int uid, int reason) {
if (mOnPartyListener != null) {
mOnPartyListener.onUserOffline(uid);
}
}
};複製程式碼
同時,我們也提供了一個介面,暴露給AgoraManager外部。
public interface OnPartyListener {
void onJoinChannelSuccess(String channel, int uid);
void onGetRemoteVideo(int uid);
void onLeaveChannelSuccess();
void onUserOffline(int uid);
}複製程式碼
在PartyRoomActivity中監聽事件
AgoraManager.getInstance()
.setupLocalVideo(getApplicationContext())
.setOnPartyListener(mOnPartyListener)//設定監聽
.joinChannel(channel)
.startPreview();複製程式碼
設定遠端使用者視訊
private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {
/**
* 獲取遠端使用者視訊的回撥
*/
@Override
public void onGetRemoteVideo(final int uid) {
//操作UI,需要切換到主執行緒
runOnUiThread(new Runnable() {
@Override
public void run() {
//設定遠端使用者的視訊
AgoraManager.getInstance().setupRemoteVideo(PartyRoomActivity.this, uid);
//將遠端使用者視訊的SurfaceView新增到PartyRoomLayout中,這會觸發PartyRoomLayout重新走一遍繪製流程
mPartyRoomLayout.addView(AgoraManager.getInstance().getSurfaceView(uid));
}
});
}
};複製程式碼
測量佈局二分屏
當第一次回撥onGetRemoteVideo時,說明現在有兩個使用者了,所以在PartyRoomLayout中需要對二分屏模式進行處理
/**
* 二分屏時的測量
*/
private void measureTwoChild(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int size = MeasureSpec.getSize(heightMeasureSpec);
//孩子高度為父容器高度的一半
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
child.measure(widthMeasureSpec, childHeightMeasureSpec);
}
}
/**
* 二分屏模式的佈局
*/
private void layoutTwoChild() {
int left = 0;
int top = 0;
int right = getMeasuredWidth();
int bottom = getChildAt(0).getMeasuredHeight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(left, top, right, bottom);
top += child.getMeasuredHeight();
bottom += child.getMeasuredHeight();
}
}複製程式碼
使用者離線時的處理
當有使用者離線時,我們需要移除該使用者視訊對應的SurfaceView
private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {
@Override
public void onUserOffline(final int uid) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//從PartyRoomLayout移除遠端視訊的SurfaceView
mPartyRoomLayout.removeView(AgoraManager.getInstance().getSurfaceView(uid));
//清除快取的SurfaceView
AgoraManager.getInstance().removeSurfaceView(uid);
}
});
}
};複製程式碼
四分屏和六分屏
當有3個或者4個老鐵開趴,介面顯示成四分屏, 當有5個或者6個老鐵開趴,介面切分成六分屏
由於之前已經處理了新進使用者就會建立SurfaceView加入PartyRoomLayout的邏輯,所以這裡只需要處理四六分屏時的測量和佈局
四六分屏測量
private void measureMoreChildSplit(int widthMeasureSpec, int heightMeasureSpec) {
//列數為兩列,計算行數
int row = getChildCount() / 2;
if (getChildCount() % 2 != 0) {
row = row + 1;
}
//根據行數平分高度
int childHeight = MeasureSpec.getSize(heightMeasureSpec) / row;
//寬度為父容器PartyRoomLayout的寬度一般,即屏寬的一半
int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}複製程式碼
四六分屏佈局
private void layoutMoreChildSplit() {
int left = 0;
int top = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
if ( (i + 1 )% 2 == 0) {//滿足換行條件,更新left和top,佈局下一行
left = 0;
top += child.getMeasuredHeight();
} else {
//不滿足換行條件,更新left值,繼續佈局一行中的下一個孩子
left += child.getMeasuredWidth();
}
}
}複製程式碼
雙擊上下分屏佈局
在四六分屏模式下,雙擊一個小窗,窗會變大,其餘小窗在底部排列, 成上下分屏模式。實現思路就是監聽PartyRoomLayout的觸控時間,當是雙擊時,則重新佈局。
觸控事件處理
/**
* 攔截所有的事件
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
/**
* 讓GestureDetector處理觸控事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
return true;
}
//四六分屏模式
private static int DISPLAY_MODE_SPLIT = 0;
//上下分屏模式
private static int DISPLAY_MODE_TOP_BOTTOM = 1;
//顯示模式的變數,預設是四六分屏
private int mDisplayMode = DISPLAY_MODE_SPLIT;
//上下分屏時上面View的下標
private int mTopViewIndex = -1;
private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
handleDoubleTap(e);//處理雙擊事件
return true;
}
private void handleDoubleTap(MotionEvent e) {
//遍歷所有的孩子
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
//獲取孩子view的矩形
Rect rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
if (rect.contains((int)e.getX(), (int)e.getY())) {//找到雙擊位置的孩子是誰
if (mTopViewIndex == i) {//如果點選的位置就是上面的view, 則切換成四六分屏模式
mDisplayMode = DISPLAY_MODE_SPLIT;
mTopViewIndex = -1;//重置上面view的下標
} else {
//切換成上下分屏模式,
mTopViewIndex = i;//儲存雙擊位置的下標,即上面View的下標
mDisplayMode = DISPLAY_MODE_TOP_BOTTOM;
}
requestLayout();//請求重新佈局
break;
}
}
}
};複製程式碼
上下分屏測量
處理完雙擊事件後,切換顯示模式,請求重新佈局,這時候又會觸發測量和佈局。
/**
* 上下分屏模式的測量
*/
private void measureMoreChildTopBottom(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
if (i == mTopViewIndex) {
//測量上面View
measureTopChild(widthMeasureSpec, heightMeasureSpec);
} else {
//測量下面View
measureBottomChild(i, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* 上下分屏模式時上面View的測量
*/
private void measureTopChild(int widthMeasureSpec, int heightMeasureSpec) {
int size = MeasureSpec.getSize(heightMeasureSpec);
//高度為PartyRoomLayout的一半
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
getChildAt(mTopViewIndex).measure(widthMeasureSpec, childHeightMeasureSpec);
}
/**
* 上下分屏模式時底部View的測量
*/
private void measureBottomChild(int i, int widthMeasureSpec, int heightMeasureSpec) {
//除去頂部孩子後還剩的孩子個數
int childCountExcludeTop = getChildCount() - 1;
//當底部孩子個數小於等於3時
if (childCountExcludeTop <= 3) {
//平分孩子寬度
int childWidth = MeasureSpec.getSize(widthMeasureSpec) / childCountExcludeTop;
int size = MeasureSpec.getSize(heightMeasureSpec);
//高度為PartyRoomLayout的一半
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
} else if (childCountExcludeTop == 4) {//當底部孩子個數為4個時
int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;//寬度為PartyRoomLayout的一半
int childHeight = MeasureSpec.getSize(heightMeasureSpec) / 4;//高度為PartyRoomLayout的1/4
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
} else {//當底部孩子大於4個時
//計算行的個數
int row = childCountExcludeTop / 3;
if (row % 3 != 0) {
row ++;
}
//孩子的寬度為PartyRoomLayout寬度的1/3
int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 3;
//底部孩子平分PartyRoomLayout一半的高度
int childHeight = (MeasureSpec.getSize(heightMeasureSpec) / 2) / row;
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}複製程式碼
上下分屏佈局
private void layoutMoreChildTopBottom() {
//佈局上面View
View topView = getChildAt(mTopViewIndex);
topView.layout(0, 0, topView.getMeasuredWidth(), topView.getMeasuredHeight());
int left = 0;
int top = topView.getMeasuredHeight();
for (int i = 0; i < getChildCount(); i++) {
//上面已經佈局過上面的View, 這裡就跳過
if (i == mTopViewIndex) {
continue;
}
View view = getChildAt(i);
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
//佈局下面的一個View
view.layout(left, top, right, bottom);
left = left + view.getMeasuredWidth();
if (left >= getWidth()) {//滿足換行條件則換行
left = 0;
top += view.getMeasuredHeight();
}
}
}複製程式碼
老鐵,一起來開Party(一) —— 聲網Agoria SDK整合
老鐵,一起來開Party(三) —— 聲網Agoria SDK趟坑記