Android 騰訊音視頻聊天的實現(xiàn)

前言

近期項目做即時聊天骤宣,以前已經(jīng)實現(xiàn)秦爆,使用環(huán)信實現(xiàn)的,當(dāng)時用時不到一個星期憔披,但是視頻通話質(zhì)量不是很好等限,老板不是很喜歡,要求換成騰訊芬膝,然后開始調(diào)研騰訊im 望门,騰訊im即時聊天的文檔中沒有音視頻通話,要自己實現(xiàn)锰霜,使用互動直播實現(xiàn)筹误,因此要實現(xiàn)這個功能就要花錢,公司有錢任性癣缅,但是自己也可以接觸下騰訊

這一番接觸下厨剪,騰訊給我的感覺并不友好,demo功能不是很全友存,很多自己實現(xiàn)祷膳,文檔寫的很不完善控乾,大數(shù)要靠自己猜樊拓,猜中了像中了500萬区匣,猜不中就跟謝謝惠顧的税产。帘撰。兄纺。吐槽無用還是要實現(xiàn)功能向族,現(xiàn)在我們來說一下實現(xiàn)音視頻通話侦讨。

騰訊專門封裝了一個callSdk 來實現(xiàn)音視頻通話吟策,Android Ios儒士,兩個客戶端都有,通過callSdk 實現(xiàn)音視頻會簡單很多檩坚,主要設(shè)置監(jiān)聽着撩,進(jìn)行業(yè)務(wù)處理

CallSdk 的api文檔:https://zhaoyang21cn.github.io/iLiveSDK_Help/callsdk/

音視頻通話的demo:https://github.com/zhaoyang21cn/CallSDK_Android_Demo

看到這個github上的demo圖片和運(yùn)行出來的圖片不符诅福,可能會傷心了,具體的邏輯要自己實現(xiàn)拖叙,demo運(yùn)行出來視頻建立后會出現(xiàn)黑屏氓润,是因為沒有開權(quán)限的問題,希望不要陷坑太久薯鳍。

互動直播的api文檔:https://zhaoyang21cn.github.io/iLiveSDK_Help/android_help/

因為用到AVRootView 控件咖气,所以參看這個文檔

先來看一下我們視頻通話實現(xiàn)的效果

視頻呼叫頁面


image.png

視頻應(yīng)答頁面


image.png

一.在gradle中加入

"com.tencent.ilivesdk:ilivesdk:1.8.6.3",
 "com.tencent.callsdk:callsdk:1.0.31",

二.音視頻的主要邏輯代碼

package com.jyjt.ydyl.txim;

import android.Manifest;
import android.app.Activity;
import android.app.Application;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.text.TextUtils;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.google.gson.JsonObject;
import com.jyjt.ydyl.BaseActivity;
import com.jyjt.ydyl.BasePresenter;
import com.jyjt.ydyl.R;
import com.jyjt.ydyl.application.MyApplication;
import com.jyjt.ydyl.tools.AppUtils;
import com.jyjt.ydyl.tools.ConfigUtils;
import com.jyjt.ydyl.tools.LogUtils;
import com.jyjt.ydyl.tools.SwitchActivityManager;
import com.jyjt.ydyl.tools.ToastUtil;
import com.jyjt.ydyl.txim.presentation.event.VcallEvent;
import com.tencent.av.sdk.AVAudioCtrl;
import com.tencent.callsdk.ILVBCallMemberListener;
import com.tencent.callsdk.ILVCallConstants;
import com.tencent.callsdk.ILVCallListener;
import com.tencent.callsdk.ILVCallManager;
import com.tencent.callsdk.ILVCallNotification;
import com.tencent.callsdk.ILVCallOption;
import com.tencent.ilivesdk.ILiveCallBack;
import com.tencent.ilivesdk.ILiveConstants;
import com.tencent.ilivesdk.ILiveSDK;
import com.tencent.ilivesdk.core.ILiveLoginManager;
import com.tencent.ilivesdk.view.AVRootView;
import com.tencent.ilivesdk.view.AVVideoView;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;

/**
 * 蘇艷
 * <p>
 * 權(quán)限校驗
 * 音視頻呼叫:呼叫的時候校驗麥克分或攝像頭權(quán)限
 * 音視頻接聽:點擊接受時校驗麥克分或攝像頭權(quán)限
 * 視頻通話:通話的時候時候校驗麥克分權(quán)限
 */


public class VoiceCallActivity extends BaseActivity implements ILVCallListener, ILVBCallMemberListener, ILiveLoginManager.TILVBStatusListener, AVRootView.onSubViewCreatedListener {
    //大小頭像以及提示語========================================================
    @BindView(R.id.rl_big_samll_head)
    RelativeLayout rl_big_samll_head;
    @BindView(R.id.tv_waring)
    TextView tv_waring;
    @BindView(R.id.iv_video_big_head)
    ImageView iv_video_big_head;
    @BindView(R.id.iv_video_small_head)
    ImageView iv_video_small_head;
    @BindView(R.id.tv_yuyin_timer)
    public TextView tv_yuyin_timer;
    //兩路視頻通話界面================================================================
    @BindView(R.id.rl_video_conversation)
    RelativeLayout rl_video_conversation;
    @BindView(R.id.iv_switch_camera)
    ImageView iv_switch_camera;
    @BindView(R.id.av_root_view)
    AVRootView av_root_view;
    //視屏呼叫界面  1================================================================
    @BindView(R.id.ll_bootom_call)
    LinearLayout ll_bootom_call;
    //視頻邀請界面 2================================================================
    @BindView(R.id.ll_bottom_invitation)
    LinearLayout ll_bottom_invitation;
    @BindView(R.id.ll_video_yes)
    LinearLayout ll_video_yes;
    @BindView(R.id.ll_video_no)
    LinearLayout ll_video_no;
    //視頻通話界面 3==========================================================
    @BindView(R.id.ll_bootom_conversation)
    LinearLayout ll_bootom_conversation;
    @BindView(R.id.ll_hangup)
    LinearLayout ll_hangup;
    @BindView(R.id.ll_no_voice)
    LinearLayout ll_no_voice;
    @BindView(R.id.ll_big_voice)
    LinearLayout ll_big_voice;
    @BindView(R.id.iv_no_voice)
    ImageView iv_no_voice;
    @BindView(R.id.iv_big_voice)
    ImageView iv_big_voice;
    //基本數(shù)據(jù)================================================================
    //1.自己的id
    private String mHostId = "";
    //2.對方的id
    private String mUserId = "";
    //3.呼叫的類型    語音電話: CALL_TYPE_AUDIO  視頻電話 : CALL_TYPE_VIDEO
    private int mCallType;
    //4.頁面類型 1:視頻呼叫 2:視頻邀請  3:視頻通話
    private int mType;
    //5.通話的id
    private int mCallId;
    //6.對方頭像的url
    private String mHeadUrl = "";
    //7.對方的名字
    private String mUserName = "";
    //8.雙人視頻通話配置
    private ILVCallOption mOption;
    //9.麥克風(fēng)切換設(shè)置
    private boolean mMicEnalbe = true;
    //10.揚(yáng)聲器切換設(shè)置
    private boolean mSpeaker = true;
    //11.默認(rèn)設(shè)置前置攝像頭
    private int mCurCameraId = ILiveConstants.FRONT_CAMERA;
    //12.計時器
    private Handler mHandler = new Handler();
    //13.當(dāng)前毫秒數(shù)
    private long currentSecond = 0;

    //=======================================================================
    @Override
    protected BasePresenter loadPresenter() {
        return null;
    }

    @Override
    protected void initData() {
    }

    @Override
    protected void initView() {
        //1.獲取頁面類型
        mType = getIntent().getIntExtra("Type", 0);
        //2.獲取type類型
        mCallType = getIntent().getIntExtra("CallType", ILVCallConstants.CALL_TYPE_VIDEO);
        //3.獲取自己的id
        mHostId = getIntent().getStringExtra("HostId");
        //4.獲取對方的id
        mUserId = getIntent().getStringExtra("UserId");
        //5.獲取通話id
        mCallId = getIntent().getIntExtra("CallId", 0);
        //6.獲取對方頭像
        mHeadUrl = getIntent().getStringExtra("HeadUrl");
        //7.獲取對方的名字,來電的時候展示
        mUserName = getIntent().getStringExtra("UserName");
        //8.初始化視頻呼叫和接聽的CallOption
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("name", ConfigUtils.getMyName());
        jsonObject.addProperty("head", ConfigUtils.getMyHead());
        String s = jsonObject.toString();
        mOption = new ILVCallOption(mHostId).callTips("").setMemberListener(this).setCallType(mCallType).customParam(s);
        //9.根據(jù)type 類型展示布局
        showLayout(mType);
    }

    @Override
    protected void initListener() {
        //1.設(shè)置視頻通話監(jiān)聽
        ILVCallManager.getInstance().addCallListener(this);
        //2.被迫下線監(jiān)聽
        ILiveLoginManager.getInstance().setUserStatusListener(this);
        //3.AVRootView設(shè)置渲染后監(jiān)聽
        av_root_view.setSubCreatedListener(this);
        //2.結(jié)束監(jiān)聽
        ll_bootom_call.setOnClickListener(this);
        //3.接受監(jiān)聽
        ll_video_yes.setOnClickListener(this);
        //4.拒絕監(jiān)聽
        ll_video_no.setOnClickListener(this);
        //5.掛斷監(jiān)聽
        ll_hangup.setOnClickListener(this);
        //6.靜音監(jiān)聽
        ll_no_voice.setOnClickListener(this);
        //7.免提監(jiān)聽
        ll_big_voice.setOnClickListener(this);
        //8.攝像頭切換
        iv_switch_camera.setOnClickListener(this);
    }

    /**
     * 根據(jù)mType 類型展示布局
     *
     * @param mType
     */
    public void showLayout(int mType) {
        switch (mType) {
            case 1:
                //1.判斷攝像頭權(quán)限
                if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
                    openCamerePermisson();
                } else {
                    openRecordPermisson();
                }
                //2.展示視頻呼叫布局
                showDefault();
                showBigSmallHead();
                ll_bootom_call.setVisibility(View.VISIBLE);
                //3.修改提示語,展示頭像
                tv_waring.setText(((mCallType == ILVCallConstants.CALL_TYPE_VIDEO) ? "視頻電話撥通中..." : "聊天撥通中..."));
                //4.呼叫
                makecall(mUserId);
                break;
            case 2:
                //1.展示視頻邀請布局
                showDefault();
                showBigSmallHead();
                ll_bottom_invitation.setVisibility(View.VISIBLE);
                //2.設(shè)置提示語
                tv_waring.setText(mUserName + ((mCallType == ILVCallConstants.CALL_TYPE_VIDEO) ? "邀請你加入視頻通話" : "邀請你加入語音通話"));
                break;
            case 3:
                //1.展示視頻通話布局
                showDefault();
                ll_bootom_conversation.setVisibility(View.VISIBLE);
                //2.根據(jù)視頻通話和語音通話展示不同的布局
                if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
                    //1.校驗麥克分權(quán)限(視頻權(quán)限在呼叫和接聽的時候校驗了)
                    openRecordPermisson();
                    //2.展示兩路視頻界面
                    rl_video_conversation.setVisibility(View.VISIBLE);
                    //3.初始化AV基礎(chǔ)
                    ILVCallManager.getInstance().initAvView(av_root_view);
                } else {
                    //1.展示大小頭像界面
                    rl_big_samll_head.setVisibility(View.VISIBLE);
                    //2.設(shè)置提示語
                    tv_waring.setText("通話中");
                }
                //3.初始化計時器
                timeRunable.run();
                break;
            default:
                break;
        }
    }

    //打開相機(jī)權(quán)限
    public void openCamerePermisson() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // api16之后需要權(quán)限
                && ActivityCompat.checkSelfPermission(VoiceCallActivity.this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {//權(quán)限不夠時,提示
            ToastUtil.setToast("請打開攝像頭權(quán)限");
        }
    }

    //打開麥克風(fēng)權(quán)限
    public void openRecordPermisson() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // api16之后需要權(quán)限
                && ActivityCompat.checkSelfPermission(VoiceCallActivity.this, Manifest.permission.RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {//權(quán)限不夠時挖滤,提示
            ToastUtil.setToast("請打開麥克風(fēng)權(quán)限權(quán)限");
        }
    }

    //展示大小頭像
    private void showBigSmallHead() {
        rl_big_samll_head.setVisibility(View.VISIBLE);
        AppUtils.loadCirclePic(this, R.mipmap.personal, mHeadUrl, iv_video_small_head);
        AppUtils.loadBitmapCicleIcon(this, R.mipmap.personal_center, mHeadUrl, iv_video_big_head);
    }

    //計時器
    private Runnable timeRunable = new Runnable() {
        @Override
        public void run() {
            currentSecond = currentSecond + 1000;
            tv_yuyin_timer.setText(getFormatHMS(currentSecond));
            //遞歸調(diào)用本runable對象崩溪,實現(xiàn)每隔一秒一次執(zhí)行任務(wù)
            mHandler.postDelayed(this, 1000);
        }
    };

    // 根據(jù)毫秒返回時分秒
    public static String getFormatHMS(long time) {
        time = time / 1000;//總秒數(shù)
        int s = (int) (time % 60);//秒
        int m = (int) (time / 60);//分
        int h = (int) (time / 3600);//秒
        return String.format("%02d:%02d:%02d", h, m, s);
    }

    /**
     * 默認(rèn)隱藏所有布局,根據(jù)需求展示
     */
    public void showDefault() {
        //1.大小頭像界面
        rl_big_samll_head.setVisibility(View.GONE);
        //2.兩路視頻界面
        rl_video_conversation.setVisibility(View.GONE);
        //3.呼叫底部
        ll_bootom_call.setVisibility(View.GONE);
        //4.邀請底部
        ll_bottom_invitation.setVisibility(View.GONE);
        //5.通話底部
        ll_bootom_conversation.setVisibility(View.GONE);
    }

    /**
     * type 為1時斩松,掉發(fā)起呼叫
     *
     * @param mUserId:對方id
     */
    public void makecall(String mUserId) {
        //1.發(fā)起呼叫生成id
        mCallId = ILVCallManager.getInstance().makeCall(mUserId + "", mOption, new ILiveCallBack() {
            @Override
            public void onSuccess(Object data) {
            }

            @Override
            public void onError(String module, int errCode, String errMsg) {
                toast(errMsg);
                finish();
            }
        });
        //2.設(shè)置呼叫類型
        VoiceService.isCallType = true;
        //3.設(shè)置通話Id
        VoiceService.isSendNOtifacation = false;

    }

    @Override
    protected int getLayoutId() {
        return R.layout.activity_voice_call;
    }

    @Override
    protected void otherViewClick(View view) {
        switch (view.getId()) {
            case R.id.ll_bootom_call://取消
                //1.結(jié)束掛斷電話
                ILVCallManager.getInstance().endCall(mCallId);
                
                break;
            case R.id.ll_video_yes://接受
                //1.判斷是否有網(wǎng)
                if (!AppUtils.isAccessNetwork(mContext)) {
                    toast("請檢查您的網(wǎng)絡(luò)");
                    return;
                }
                //2.權(quán)限驗證
                if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
                    openCamerePermisson();
                } else {
                    openRecordPermisson();
                }
                //3.接受電話
                ILVCallManager.getInstance().acceptCall(mCallId, mOption);
                break;
            case R.id.ll_video_no://拒絕
                //1.若沒有啟動過app啟動app
                showMainActivity();
                //2.拒絕通知對方已拒絕
                ILVCallManager.getInstance().rejectCall(mCallId);
                

                break;
            case R.id.ll_hangup://掛斷
                //1.應(yīng)答界面需要校驗是否啟動過app
                if (!TextUtils.isEmpty(mUserName)) {
                    showMainActivity();
                }
                //2.通話掛斷
                ILVCallManager.getInstance().endCall(mCallId);
             
                break;
            case R.id.ll_no_voice: //靜音
                changeMic();
                break;
            case R.id.ll_big_voice: //免提
                changeSpeaker();
                break;
            case R.id.iv_switch_camera: //攝像頭切換
                switchCamera();
                break;
            default:
                break;
        }

    }

    private void showMainActivity() {
        List<Activity> activityList = MyApplication.getmApplication().getActivityList();
        int size = activityList.size();
        boolean isLanch = false;
        for (int i = 0; i < size; i++) {
            LogUtils.d("suyan", "=========棧里的acivity" + activityList.get(i).getLocalClassName());
            if (activityList.get(i).getLocalClassName().equals("activity.MainActivity")) {
                isLanch = true;
                break;
            }
        }
        if (!isLanch) {
            LogUtils.d("suyan", "=========啟動");
            SwitchActivityManager.startMainActivity(mContext);
        }
    }

    //麥克風(fēng)切換
    private void changeMic() {
        if (mMicEnalbe) {
            ILVCallManager.getInstance().enableMic(false);
        } else {
            ILVCallManager.getInstance().enableMic(true);
        }
        mMicEnalbe = !mMicEnalbe;
        //關(guān)閉麥克風(fēng)
        iv_no_voice.setImageResource(mMicEnalbe ? R.mipmap.jingyin_f : R.mipmap.jingyin_t);
    }

    // 揚(yáng)聲器切換
    private void changeSpeaker() {
        if (mSpeaker) {
            ILiveSDK.getInstance().getAvAudioCtrl().setAudioOutputMode(AVAudioCtrl.OUTPUT_MODE_HEADSET);
        } else {
            ILiveSDK.getInstance().getAvAudioCtrl().setAudioOutputMode(AVAudioCtrl.OUTPUT_MODE_SPEAKER);
        }
        mSpeaker = !mSpeaker;
        iv_big_voice.setImageResource(mSpeaker ? R.mipmap.hands_free_f : R.mipmap.hands_free_t);
    }

    //切換攝像頭
    private void switchCamera() {
        mCurCameraId = (ILiveConstants.FRONT_CAMERA == mCurCameraId) ? ILiveConstants.BACK_CAMERA : ILiveConstants.FRONT_CAMERA;
        ILVCallManager.getInstance().switchCamera(mCurCameraId);
    }

    @Override
    protected void onResume() {
        ILVCallManager.getInstance().onResume();
        super.onResume();
    }

    @Override
    protected void onPause() {
        ILVCallManager.getInstance().onPause();
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        ILVCallManager.getInstance().removeCallListener(this);
        ILVCallManager.getInstance().onDestory();
        tv_yuyin_timer = null;
        timeRunable = null;
        mHandler.removeMessages(0);

        super.onDestroy();
    }

    @Override
    public void showLoading() {

    }

    @Override
    public void hideLoading() {

    }

    //攝像頭事件
    @Override
    public void onCameraEvent(String id, boolean bEnable) {

    }

    //麥克風(fēng)事件
    @Override
    public void onMicEvent(String id, boolean bEnable) {

    }

    //通話建立成功伶唯,視頻呼叫時回來的監(jiān)聽
    @Override
    public void onCallEstablish(int callId) {
        //1.修改mType類型
        mType = 3;
        //2.建立通話界面
        showLayout(mType);
        //3. 交換兩路視頻
        av_root_view.swapVideoView(0, 1);

    }

    //通話結(jié)束
    @Override
    public void onCallEnd(int callId, int endResult, String endInfo) {
        finish();
    }

    //通話異常
    @Override
    public void onException(int iExceptionId, int errCode, String errMsg) {
        toast(errMsg);
        finish();
    }

    /**
     * 監(jiān)聽Back鍵按下事件
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
            if (mType == 1) { //取消通話
                ILVCallManager.getInstance().endCall(mCallId);
            } else if (mType == 2) {//拒絕通話
                ILVCallManager.getInstance().rejectCall(mCallId);
            } else if (mType == 3) { //結(jié)束通話
                ILVCallManager.getInstance().endCall(mCallId);
            }
        }
        return super.onKeyDown(keyCode, event);


    }

    @Override
    public void onForceOffline(int error, String message) {
        //1.被迫下線監(jiān)聽提示
        toast("您的賬號在其他地方登陸");
        //2.關(guān)閉界面
        finish();
    }

    //渲染后監(jiān)聽設(shè)置拖動已經(jīng)切換兩路視頻
    @Override
    public void onSubViewCreated() {
        LogUtils.d("suyan", "====視頻建立" + ILiveLoginManager.getInstance().getMyUserId() + "=" + av_root_view.getViewByIndex(0).getIdentifier() + "/" + av_root_view.getViewByIndex(1).getIdentifier());
        //4.設(shè)置點擊小屏切換及可拖動
        for (int i = 1; i < ILiveConstants.MAX_AV_VIDEO_NUM; i++) {
            final int index = I;
            AVVideoView minorView = av_root_view.getViewByIndex(i);
            if (ILiveLoginManager.getInstance().getMyUserId().equals(minorView.getIdentifier())) {
                minorView.setMirror(true);      // 本地鏡像
            }
            minorView.setDragable(true);    // 小屏可拖動
            minorView.setGestureListener(new GestureDetector.SimpleOnGestureListener() {
                @Override
                public boolean onSingleTapConfirmed(MotionEvent e) {
                    av_root_view.swapVideoView(0, index);     // 與大屏交換
                    return false;
                }
            });
        }
    }
}

activity_voice_call 的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#66000000"
    tools:context="com.jyjt.ydyl.txim.VoiceCallActivity">

    <!--大小頭像 ,呼叫提示-->
    <RelativeLayout
        android:id="@+id/rl_big_samll_head"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--大頭像-->
        <ImageView
            android:id="@+id/iv_video_big_head"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop" />

        <View
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/frame_blcak" />
        <!--小頭像-->
        <ImageView
            android:id="@+id/iv_video_small_head"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="125dp"
            android:scaleType="centerCrop" />
        <!--通話計時器-->
        <TextView
            android:id="@+id/tv_yuyin_timer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/iv_video_small_head"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:textColor="@color/white"
            android:textSize="15sp" />

        <TextView
            android:id="@+id/tv_waring"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/tv_yuyin_timer"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="3dp"
            android:text="通話中"
            android:textColor="@color/white"
            android:textSize="15sp" />
    </RelativeLayout>
    <!--兩路視頻通話界面-->
    <RelativeLayout
        android:id="@+id/rl_video_conversation"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        android:visibility="visible">

        <com.tencent.ilivesdk.view.AVRootView
            android:id="@+id/av_root_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <!--切換攝像頭-->
        <ImageView
            android:id="@+id/iv_switch_camera"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_alignParentRight="true"
            android:layout_marginRight="20dp"
            android:layout_marginTop="20dp"
            android:src="@mipmap/ic_sw_camer" />
    </RelativeLayout>
    <!--底部惧盹,視屏呼叫界面,掛斷-->
    <LinearLayout
        android:id="@+id/ll_bootom_call"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="20dp"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/btn_hangup_call"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@mipmap/huang_up" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:text="掛斷"
            android:textColor="@color/white"
            android:textSize="12sp" />

    </LinearLayout>
    <!--底部乳幸,視屏邀請界面-->
    <LinearLayout
        android:id="@+id/ll_bottom_invitation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="20dp"
        android:orientation="horizontal">

        <!--掛斷-->
        <LinearLayout
            android:id="@+id/ll_video_no"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_marginLeft="22dp"
            android:orientation="vertical"
            android:visibility="visible">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:src="@mipmap/huang_up" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="掛斷"
                android:textColor="@color/white"
                android:textSize="12sp" />

        </LinearLayout>

        <View
            android:layout_width="0dp"
            android:layout_height="1dp"
            android:layout_weight="1" />
        <!--接聽-->
        <LinearLayout
            android:id="@+id/ll_video_yes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="22dp"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/btn_answer_call"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/jietong" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="接通"
                android:textColor="@color/white"
                android:textSize="12sp" />

        </LinearLayout>

    </LinearLayout>
    <!--底部,視頻通話界面,靜音钧椰,掛斷粹断,免提-->
    <LinearLayout
        android:id="@+id/ll_bootom_conversation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="20dp"
        android:orientation="horizontal">
        <!--靜音-->
        <LinearLayout
            android:id="@+id/ll_no_voice"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_no_voice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                android:src="@mipmap/jingyin_f" />

            <TextView
                android:id="@+id/tv_no_voice"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="@string/mute"
                android:textColor="@color/white"
                android:textSize="12sp" />
        </LinearLayout>
        <!--掛斷-->
        <LinearLayout
            android:id="@+id/ll_hangup"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_weight="1"
            android:orientation="vertical"
            android:visibility="visible">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:src="@mipmap/huang_up" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="掛斷"
                android:textColor="@color/white"
                android:textSize="12sp" />

        </LinearLayout>
        <!--免提-->
        <LinearLayout
            android:id="@+id/ll_big_voice"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_big_voice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                android:src="@mipmap/hands_free_f" />

            <TextView
                android:id="@+id/tv_big_voice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="@string/Hands_free"
                android:textColor="@color/white"
                android:textSize="12sp" />
        </LinearLayout>
    </LinearLayout>
</RelativeLayout>

三.視頻呼叫

視頻呼叫啟動視頻呼叫界面

 public void callVideo() {
        if (TextUtils.isEmpty(user_id) || TextUtils.isEmpty(ConfigUtils.getUid())) {
            toast("ID為空");
            return;
        }
       
     startVoiceCallActivity(mContext, ConfigUtils.getUid(), user_id, 1, TXChatHead.getInstance().getUserHead(user_id) == null ? "" : TXChatHead.getInstance().getUserHead(user_id), ILVCallConstants.CALL_TYPE_VIDEO);
    }
 /**
     * 音視頻界面
     *
     * @param HostId :自己的id,由服務(wù)器生成的
     * @param UserId :對方的id嫡霞,由服務(wù)器生成的
     * @param Type   :頁面的類型 1:視頻呼叫 2:視頻邀請  3:視頻通話,本方法只能傳1
     */
    public static void startVoiceCallActivity(Context context, String HostId, String UserId, int Type, String HeadUrl, int CallType) {
        Intent intent = new Intent(context, VoiceCallActivity.class);
        intent.putExtra("HostId", HostId);
        intent.putExtra("CallType", CallType);
        intent.putExtra("UserId", UserId);
        intent.putExtra("Type", Type);
        intent.putExtra("HeadUrl", HeadUrl);
        context.startActivity(intent);
        ((Activity) context).overridePendingTransition(R.anim.left_out, R.anim.left_in);
    }

四.應(yīng)答界面主要邏輯

應(yīng)答界面姿染,思路,callsdk 提供了來電監(jiān)聽ILVIncomingListener秒际,實現(xiàn)了這個監(jiān)聽就能收到所有來電,但是有個問題這個監(jiān)聽寫在哪里能讓整個app 都收到狡汉,所以我寫在了服務(wù)里面
注冊服務(wù)器

<service android:name="com.jyjt.ydyl.txim.VoiceService" />

在應(yīng)用啟動的時候打開服務(wù)

 Intent serviceIntent = new Intent(MainActivity.this, VoiceService.class);
        startService(serviceIntent);

服務(wù)中的應(yīng)答邏輯

package com.jyjt.ydyl.txim;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;

import com.jyjt.ydyl.application.MyApplication;
import com.jyjt.ydyl.tools.ConfigUtils;
import com.jyjt.ydyl.tools.LogUtils;
import com.jyjt.ydyl.tools.SwitchActivityManager;
import com.jyjt.ydyl.tools.ToastUtil;
import com.jyjt.ydyl.txim.model.TXChatHead;
import com.jyjt.ydyl.txim.presentation.event.VcallEvent;
import com.tencent.callsdk.ILVCallConfig;
import com.tencent.callsdk.ILVCallConstants;
import com.tencent.callsdk.ILVCallListener;
import com.tencent.callsdk.ILVCallManager;
import com.tencent.callsdk.ILVCallNotification;
import com.tencent.callsdk.ILVCallNotificationListener;
import com.tencent.callsdk.ILVIncomingListener;
import com.tencent.callsdk.ILVIncomingNotification;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;

/**
 * 視頻邀請監(jiān)聽
 * 1.服務(wù)在閃屏頁面啟動
 * <p>
 * 蘇艷
 **/
public class VoiceService extends Service implements ILVIncomingListener, ILVCallListener, ILVCallNotificationListener {
    //1.是否已經(jīng)啟動了應(yīng)答界面
    boolean isStartActivity = false;
    //2.是否是主動呼叫娄徊,true :呼叫  false:應(yīng)答
    public static boolean isCallType = false;
    //3.是否有心跳,true:有心跳  false :沒有心跳
    public boolean havaHeardJump = false;
    //4.本次會話是否發(fā)過通知,默認(rèn)沒有發(fā)過
    public static boolean isSendNOtifacation = false;
    //5.通話后對方掛斷盾戴,保留通話時長
    public static String mCallTime = "";
    //6.視頻通話建立的時間點
    long mStartTime;
    //7.狀態(tài)碼和對應(yīng)的提示文字
    public static HashMap<Integer, String> mWraingHashMap = new HashMap();

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        //1.設(shè)置監(jiān)聽
        ILVCallManager.getInstance().addCallListener(this);
        ILVCallManager.getInstance().addIncomingListener(this);
      ILVCallManager.getInstance().init(new ILVCallConfig().setAutoBusy(true).setNotificationListener(this));
        //2.設(shè)置通知提示語
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_DISCONNECT, "通話被服務(wù)器回收");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_FAILED, "請求失敗");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_HANGUP, "通話結(jié)束");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_LOCAL_CANCEL, "聊天已取消");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_NOT_EXIST, "通話不存在");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_RESPONDER_LINEBUSY, "接聽方占線");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_RESPONDER_REFUSE, "已拒絕");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_SPONSOR_CANCEL, "呼叫方取消");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_SPONSOR_TIMEOUT, "呼叫方超時");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_Hangup, "對方結(jié)束通話");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_Reject, "對方已拒絕");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_SponsorCancel, "對方取消");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_SponsorTimeout, "無人接聽");
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        ILVCallManager.getInstance().removeIncomingListener(this);
        super.onDestroy();
    }

    @Override
    public void onNewIncomingCall(final int callId, final int callType, final ILVIncomingNotification notification) {
        Log.i("suyan", "========接聽電話+service" + callId + "=" + callType + "=" + notification.getSponsorId() + "=自己的id==");

        if (callId != 0 && notification != null) {
            //1.判斷呼叫是否超時,超時時間30m
            if ((System.currentTimeMillis() / 1000) - notification.getTimeStamp() > 30) {
                return;
            }
            //2.保存通話的id
            isSendNOtifacation = false;
            //3.延遲兩面后判斷是否有心跳
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (havaHeardJump) {
                        //4.啟動應(yīng)答界面
                        try {
                            isStartActivity = true;
                            isCallType = false;
                            String json = ((ILVCallNotification) notification).getUserInfo();
                            JSONObject jsonObj = new JSONObject(json);
                            String name = jsonObj.getString("name");
                            String head = jsonObj.getString("head");
                            //5.保存頭像和昵稱寄锐,以防止首次聊天,沒有存過頭像和昵稱
                            TXChatHead.getInstance().addHead(head, notification.getSponsorId() + "", name);
                            //6.啟動應(yīng)答界面
                            SwitchActivityManager.startNewTaskVoiceCallActivity(VoiceService.this, 2, callId, ConfigUtils.getUid(), notification.getSponsorId(), TextUtils.isEmpty(head) ? "" : head, TextUtils.isEmpty(name) ? "" : name, callType);
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, 2000);


        }

    }


    @Override
    public void onCallEstablish(int callId) {
        mStartTime = System.currentTimeMillis();
    }

    @Override
    public void onCallEnd(int callId, int endResult, String endInfo) {
        LogUtils.d("suyan", "========結(jié)束通話111");
        havaHeardJump = false;
        isSendNOtifacation = false;
        //關(guān)閉視頻通話界面,處理弱網(wǎng)情況視頻邀請喚起尖啡,但是對方已掛斷問題
        if (isStartActivity) {
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    MyApplication.getmApplication().finishActivity(VoiceCallActivity.class);
                    isStartActivity = false;
                }
            }, 503);

        }

    }

    @Override
    public void onException(int iExceptionId, int errCode, String errMsg) {
    }


    @Override
    public void onRecvNotification(int callid, ILVCallNotification notification) {
        //心跳 10s  超時30s
        ILVCallNotification notification = (ILVCallNotification) arg;
        LogUtils.d("suyan", "===========通知的id" + notification.getNotifId() + "=====時間戳" + notification.getTimeStamp());
        //1.是否有過心跳
        if (notification.getNotifId() == ILVCallConstants.TCILiveCMD_HeartBeat) {
            havaHeardJump = true;
        }
        //3.設(shè)置提示語
        String waring = "";
        waring = mWraingHashMap.get(notification.getNotifId());
        String tostString = "";
        switch (notification.getNotifId()) {
            case ILVCallConstants.ERR_CALL_HANGUP: // 4 通話后橄仆,掛斷,顯示市場衅斩,自己發(fā)的通知
                waring = "聊天時長" + (TextUtils.isEmpty(notification.getUserInfo()) ? "" : notification.getUserInfo());
                tostString = "通話結(jié)束";
                break;
            case ILVCallConstants.TCILiveCMD_Hangup: //134  我呼叫盆顾,接通后對方掛斷
                Long mCallTime = System.currentTimeMillis() - mStartTime;
                String formatHMS = getFormatHMS(mCallTime);
                waring = TextUtils.isEmpty(formatHMS) ? "對方結(jié)束通話" : "聊天時長" + formatHMS;
                tostString = "對方結(jié)束通話";
                break;
            default:
                break;
        }

        if (!TextUtils.isEmpty(waring) && !isSendNOtifacation) {
            //1.發(fā)本地消息,isCallType=true:我主動呼叫消息由我發(fā)出  isCallType=false:我應(yīng)答消息消息有對方發(fā)出
            waring = "[音視頻通話]" + waring;
            //次方法是給在會話中存一條本地消息畏梆,在會話界面中顯示您宪,
//ChatActivity.saveLocalMessage(notification.getSender(), waring, isCallType, isCallType);
            //2.本次通話已發(fā)送過通知
            isSendNOtifacation = true;
            //3.toast 提示
            if (isStartActivity || isCallType) {
                ToastUtil.setToast((notification.getNotifId() == ILVCallConstants.TCILiveCMD_Hangup || notification.getNotifId() == ILVCallConstants.ERR_CALL_HANGUP) ? tostString : waring);

            }
        }
    }

    // 根據(jù)毫秒返回時分秒
    public String getFormatHMS(long time) {
        time = time / 1000;//總秒數(shù)
        int s = (int) (time % 60);//秒
        int m = (int) (time / 60);//分
        int h = (int) (time / 3600);//秒
        return String.format("%02d:%02d:%02d", h, m, s);
    }
}

五.跳轉(zhuǎn)應(yīng)答界面

/**
     * 視頻邀請界面
     *
     * @param mType   :頁面的類型 1:視頻呼叫 2:視頻邀請  3:視頻通話
     * @param mCallId :會話id
     */
    public static void startNewTaskVoiceCallActivity(Context context, int mType, int mCallId, String mHostId, String mUserId, String mHeadUrl, String mName, int CallType) {
        Intent intent = new Intent(context, VoiceCallActivity.class);
        intent.setAction(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("CallType", CallType);
        intent.putExtra("Type", mType);
        intent.putExtra("CallId", mCallId);
        intent.putExtra("HostId", mHostId);
        intent.putExtra("HeadUrl", mHeadUrl);
        intent.putExtra("UserName", mName);
        intent.putExtra("UserId", mUserId);
        context.startActivity(intent);
    }

因為在service里面啟動app奈懒,有可能這個app還沒有啟動沒有棧,所以要單獨開個棧宪巨,設(shè)置flag Intent.FLAG_ACTIVITY_NEW_TASK

六.總結(jié)

以上就是音視頻通話的所有邏輯磷杏,主要都是參考callsdk中的demo 和callSdk的api文檔去實現(xiàn)。其中有幾個問題是demo中沒有的要自己實現(xiàn)捏卓,本代碼都已經(jīng)實現(xiàn)
1.通話狀態(tài)通知自己獲取添加
2.呼叫极祸,來電鈴聲
3.來電亮屏
4.通過心跳處理有效來電

總的來說,騰訊的音視頻通話怠晴,大部分功能要自己實現(xiàn)遥金,不像環(huán)信哪樣細(xì)節(jié)都已經(jīng)處理好了,所以自己實現(xiàn)時龄寞,要多考慮細(xì)節(jié)問題汰规,callSdk Demo 很簡單,只是實現(xiàn)了基本功能其余細(xì)小功能要多留意物邑,api文檔中有提供相應(yīng)方法的調(diào)用溜哮,要參考文檔去實現(xiàn)。什么SDK都不會保證百分之百不會出現(xiàn)問題色解,目前發(fā)現(xiàn)騰訊有時登錄會失敗茂嗓,需要自己進(jìn)行二次處理。

2018.11.16

最近得知callSdk 下架了科阎,鏈接找不到callsdk 的demo 述吸,咨詢了互動直播的技術(shù)人員,的確如此锣笨。
回復(fù)如下:


image.png

經(jīng)過咨詢?nèi)绻麑崿F(xiàn)音視頻通話蝌矛,可以參考下面Demo。

Android_TRTC

騰訊實時音視頻(TRTC)错英,集成了賬號登錄入撒、音視頻通話、文本消息聊天等基礎(chǔ)功能椭岩,可在無音視頻基礎(chǔ)技術(shù)的情況下茅逮,快速接入開發(fā)定制化的實時音視頻產(chǎn)品。

參考地址:
https://github.com/zhaoyang21cn/Android_TRTC

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末判哥,一起剝皮案震驚了整個濱河市献雅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌塌计,老刑警劉巖挺身,帶你破解...
    沈念sama閱讀 221,331評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锌仅,居然都是意外死亡瞒渠,警方通過查閱死者的電腦和手機(jī)良蒸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伍玖,“玉大人嫩痰,你說我怎么就攤上這事∏瞎浚” “怎么了串纺?”我有些...
    開封第一講書人閱讀 167,755評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長椰棘。 經(jīng)常有香客問我纺棺,道長,這世上最難降的妖魔是什么邪狞? 我笑而不...
    開封第一講書人閱讀 59,528評論 1 296
  • 正文 為了忘掉前任祷蝌,我火速辦了婚禮,結(jié)果婚禮上帆卓,老公的妹妹穿的比我還像新娘巨朦。我一直安慰自己,他們只是感情好剑令,可當(dāng)我...
    茶點故事閱讀 68,526評論 6 397
  • 文/花漫 我一把揭開白布糊啡。 她就那樣靜靜地躺著,像睡著了一般吁津。 火紅的嫁衣襯著肌膚如雪棚蓄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,166評論 1 308
  • 那天碍脏,我揣著相機(jī)與錄音梭依,去河邊找鬼。 笑死典尾,一個胖子當(dāng)著我的面吹牛睛挚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播急黎,決...
    沈念sama閱讀 40,768評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侧到!你這毒婦竟也來了勃教?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,664評論 0 276
  • 序言:老撾萬榮一對情侶失蹤匠抗,失蹤者是張志新(化名)和其女友劉穎故源,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汞贸,經(jīng)...
    沈念sama閱讀 46,205評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡绳军,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,290評論 3 340
  • 正文 我和宋清朗相戀三年印机,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片门驾。...
    茶點故事閱讀 40,435評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡射赛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奶是,到底是詐尸還是另有隱情楣责,我是刑警寧澤,帶...
    沈念sama閱讀 36,126評論 5 349
  • 正文 年R本政府宣布聂沙,位于F島的核電站秆麸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏及汉。R本人自食惡果不足惜沮趣,卻給世界環(huán)境...
    茶點故事閱讀 41,804評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坷随。 院中可真熱鬧房铭,春花似錦、人聲如沸甸箱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芍殖。三九已至豪嗽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豌骏,已是汗流浹背龟梦。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留窃躲,地道東北人计贰。 一個月前我還...
    沈念sama閱讀 48,818評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像蒂窒,于是被迫代替她去往敵國和親躁倒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,442評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,266評論 25 707
  • 用兩張圖告訴你洒琢,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料秧秉? 從這篇文章中你...
    hw1212閱讀 12,744評論 2 59
  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,986評論 3 119
  • 更: 側(cè)邊任務(wù)欄3D Touch iOS11.1已回歸 1衰抑,以前的語境里象迎,點擊高于滑動,點擊包含滑動 iOS:按鍵...
    JackYan閱讀 520評論 0 0