云之讯官方测试Demo音频版源码阅读(编辑)
生活随笔
收集整理的這篇文章主要介紹了
云之讯官方测试Demo音频版源码阅读(编辑)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
* 由于最近項目中貌似需要做這一塊,于是就去讀了一下云之訊官方測試Demo的音頻版的源碼[Android音頻版](http://www.ucpaas.com/product_service/download)。
* 其實這個Demo并不是什么高大上的代碼,也沒有很多生澀難懂的代碼在里面,相反,讀起來還是很輕松的。
* 不過,雖然代碼結構清晰,但是里面的廣播實在太多。如果不去理一理,確實不太好明白到底流程是怎樣的。不過,好在Demo中還提供了一個Activity的結構圖,也是輔助大家去做進一步的理解。
* 基于這些輔助,以及代碼中的關鍵注釋,差不多將其中的操作流程理清楚了。這里就貼出來欣賞欣賞這樣一個Demo的邏輯流程。不過由于其中的代碼太多,我只讀了Activity結構圖的第一條線的代碼。(網絡電話)這條線相對簡單,其他的線代碼量更大,就沒有去讀了。
* application logic
1. 當application被打開的時候,就開啟了一個服務ConnectionService.{ConnectionService服務的邏輯:* 1.在服務創建的時候,就添加了一些監聽器,注冊了廣播接收器* 1.1.添加若干監聽器: 連接的監聽,消息的監聽,通話的監聽* 1.2.開啟了一個廣播接收器,接收4條廣播。接收廣播: UIDfineAction.ACTION_LOGIN&& UIDfineAction.ACTION_DIAL&& UCSService.ACTION_INIT_SUCCESS&& android.intent.action.ACTION_SHUTDOWN // 系統關機廣播* 2.在該廣播中,處理邏輯為:* 1. action== UIDfineAction.ACTION_LOGIN;* 1. 如果當前有通話,則掛斷;* 2.從收到的意圖對象intent中獲取傳遞的cliend_id,cliend_pwd,sid,sid_pwd的值,并賦值給當前類定義的成員變量!* 3. 根據這4個值,分情況進行不同方式的(非token方式/token方式)“通接云之訊通信平臺”。1. 4個值都有,就進行非token的方式進行連接->開子線程,調用sdk提供的(非token的方式)connect函數,進行連接云之訊通信平臺。2. 4個值,cliend_id與cliend_pwd沒有,進行token方式的連接->開子線程,調用sdk提高的(token方式的)connect函數,進行連接云之訊通信平臺。============ACTION_LOGIN的邏輯處理完畢。2. action== UIDfineAction.ACTION_DIAL1. 從收到的意圖對象intent中獲取傳遞的type,call_uid,call_phone的值。2. 根據type的值,做不同的邏輯操作。// type: 0:直撥 1:免費 2:回撥 3:視頻點對點 4:會議 5:智能撥打case 0://0:直撥UCSCall.dial(ConnectionService.this, CallType.DIRECT, phone);case 1://1:免費UCSCall.dial(ConnectionService.this, CallType.VOIP, uid,"歡迎加入云之訊"); case 2: //2:回撥UCSCall.callBack(ConnectionService.this, phone, fromSerNum,toSerNum);case 3: 無邏輯case 4:1. 根據傳遞的意圖對象intent獲取callType的值。2. 根據callType值的不同,分別開啟不同的UCSCall.startChatRoom(,callType,);case 5:UCSCall.dial(ConnectionService.this, CallType.CALL_AUTO,phone);==================ACTION_DIAL的邏輯處理完畢。3. action== android.intent.action.ACTION_SHUTDOWN1.停止響鈴2.斷開通話==================ACTION_SHUTDOWN的邏輯處理完畢。3.onConnectionFailed(UcsReason reason)連接失敗或斷線的回調方法的邏輯處理:1. 取消定時器,將定時器置空(// TODO ------------->)2. 發送廣播意圖動作action==UIDfineAction.ACTION_TCP_LOGIN_CLIENT_RESPONSE,并攜帶數據(UIDfineAction.RESULT_KEY, 1),以及(UIDfineAction.REASON_KEY, reason.getReason());3. 根據該回調方法的參數reason的getReason()獲取的值,去判斷,如果getReason()==300505||300207,就發送廣播,發送的廣播意圖動作action==UIDfineAction.ACTION_LOGOUT,并攜帶數據(UIDfineAction.REASON_KEY, reason.getReason());并創建意圖進行Activity跳轉,跳轉到TerminalLoginDialogActivity,同時攜帶數據("reason",reason.getReason());4.onConnectionSuccessful()連接成功回調方法的處理邏輯1. 將IMMessageActivity.msgList 中的數據清空2. 發送廣播,action== UIDfineAction.ACTION_TCP_LOGIN_CLIENT_RESPONSE,并攜帶數據(UIDfineAction.RESULT_KEY, 0);3. 判斷當前成員變量cliend_id有沒有被賦值,如果有,就保存到sp文件中(文件名:yunzhixun_demo);5.onAlerting(String arg0)對方正在響鈴的回調方法的邏輯處理1. 發送廣播,action== UIDfineAction.ACTION_DIAL_STATE,并攜帶數據("state", UCSCall.CALL_VOIP_RINGING_180);6.onAnswer(String arg0)對方接通的回調方法的邏輯處理1. 發送廣播,action== UIDfineAction.ACTION_ANSWER2. 開啟定時器記錄通話時長,每隔1秒發送一次廣播action== UIDfineAction.ACTION_CALL_TIME,并攜帶數據("callduration",hour * 3600 + minute * 60 + second),以及("timer", timer.toString());7.onDialFailed(String arg0, UcsReason reason)撥打失敗的回調方法的邏輯處理1. 根據reason.getReason()的值分別發送不同的廣播:case 300210:sendBroadcast(new Intent(UIDfineAction.ACTION_DIAL_STATE).putExtra("state", UCSCall.CALL_VOIP_ERROR));break;case 300211:sendBroadcast(new Intent(UIDfineAction.ACTION_DIAL_STATE).putExtra("state", UCSCall.CALL_VOIP_NOT_ENOUGH_BALANCE));break;case 300212:sendBroadcast(new Intent(UIDfineAction.ACTION_DIAL_STATE).putExtra("state", UCSCall.CALL_VOIP_BUSY));break;case 300213:xxxx,后面還有很多case 不一一列舉======反正都是發廣播說明撥打失敗的原因8. onHangUp(String arg0, final UcsReason reason) 回調方法的邏輯處理1. 根據arg0的值是否為空,并且是否與成員變量lastIncomingCallId相等,并且當前系統時間毫秒值與lastIncomingCallTime相差不到1000毫秒Y 如果以上成立,就過1000-(系統當前時間毫秒值與lastIncomingCallTime的差值)之后,停止響鈴,并發送廣播,發送廣播的action== UIDfineAction.ACTION_DIAL_HANGUP,并攜帶數據("state", reason.getReason());N 如果以上不成了,就直接發送廣播,action== UIDfineAction.ACTION_DIAL_HANGUP,并攜帶數據("state", reason.getReason());2. 走onDialFailed(String arg0, UcsReason reason)撥打失敗的回調中相同的邏輯(發送廣播說明失敗的原因)3. 取消定時器(onAnswer(String arg0)對方接通的回調方法中創建的定時器)9. onIncomingCall(String callId, String callType,String callerNumber, String nickName, String userdata) 回調方法的邏輯處理:1. 休眠1秒2. lastIncomingCallId = callId;將callId賦值給成員變量lastIncomingCallId3. lastIncomingCallTime = System.currentTimeMillis();將系統當前時間的毫秒值賦值給成員變量lastIncomingCallTime4. 根據傳入的callType判斷,如果callType.equals("0"),就開啟Activity-->AudioConverseActivity如果callType.equals("2"),就開啟Activity-->ConferenceConverseActivity//會議無論開啟那個Activity,都攜帶數據("phoneNumber", callerNumber),以及("inCall", true),以及("nickName", nickName);10. onReceiveUcsMessage(UcsReason reason, UcsMessage message) 接收新消息回調方法的邏輯處理:1. 如果reason.getReason() == 0,就:將信息存放到IMMessageActivity.msgList集合中否則:log輸出下載文件失敗...11.xxx后面還有3,4個回調方法,不一一解釋了 }---------------------
2. 當application打開的時候,不僅開啟了上面的服務,還打開的歡迎界面,在歡迎界面停留了兩秒鐘之后,自動跳轉到了登錄界面LoginActivity
3. LoginActivity的邏輯{1.LoginActivity.onCreate(bundle)方法1. 注冊廣播接收器,接收如下廣播:接收廣播: UIDfineAction.ACTION_TCP_LOGIN_RESPONSE&&UIDfineAction.ACTION_TCP_LOGIN_CLIENT_RESPONSE2. 實例化用戶名和密碼的輸入框EditText3. 從sp文件中獲取存放過的用戶名和密碼。(首次登錄就獲取不到)Y 如果獲取到了:就判斷開啟當前Activity的意圖有沒有攜帶數據("AutoLogin", false),并且值為true,同時,成員變量mLoginDialog==null,那么,就1. 判斷一下當前的用戶是個人開發者還是企業開發者。2. 開啟一個定時器,30秒后發送一個what=0的message給handler。3. 彈出對話框顯示"正在獲取測試賬號,請稍等...",并立即關閉對話框。4. 開啟一個匿名子線程,在子線程中做如下邏輯:1. 判斷當前開發者是個人開發者還是企業開發者2. 將用戶名和密碼以及時間戳等參數以post的方式,發送http請求給指定的URL,獲取一個response對象,通過response對象,獲取其中的json字符串。3. 對獲取的json字符串進行操作0. 設立局部變量result=1; 0.5 判斷該json是否含有節點resp,如果有進行后續1,如果沒有,不走1。1. 判斷該json中是否有節點respCode,并且節點值為equals("000000"),如果成立,就將json串中的sid,token,appId,client,client_number,client_pwd,mobile等信息寫進文件 xxx/config.properties中。并將局部變量result賦值為0;如果不成立,就判斷當前json是否包含節點respCode,并將節點值轉車int賦值給result2. 最后,無論有沒有走1,都會做一件事:發送廣播發送廣播的action== UIDfineAction.ACTION_TCP_LOGIN_RESPONSE,并攜帶數據(UIDfineAction.RESULT_KEY, result);N 如果沒有獲取到:就不做任何邏輯操作。4. 綁定登錄按鈕的點擊事件:1.登錄按鈕的點擊事件的邏輯:1.檢查網絡,如果沒有聯網,吐司提醒網絡異常;如果有聯網,就走上面獲取到sp中【用戶名和密碼,并且("AutoLogin", false),的值為true,mLoginDialog==null,】的相同邏輯上面的1,2,3,4(4.1,4.2,4.3)全部的邏輯。(只是這里的用戶名和密碼是用戶輸入的,不是sp文件中取出來的)5. 綁定注冊按鈕的點擊事件:1.注冊按鈕的點擊事件的邏輯:直接跳轉到RegisterActivity界面,無他。6. 實例化顯示當前版本號的TextView,并賦值。2. onDestory 方法:0.關閉顯示正在登錄的對話框1.反注冊廣播接收器2.停止登陸超時計時器3. handlermessage 方法的處理邏輯:1. 收到msg.what==0時1.關閉顯示正在登錄的對話框2.吐司顯示登錄失敗4. 廣播接收器里面的處理邏輯:1.action== UIDfineAction.ACTION_TCP_LOGIN_RESPONSE:{在登錄按鈕點擊或者當前Activity創建的之后,會發送出去}1. 收到該廣播之后,先判斷(null==mLoginDialog || !mLoginDialog.isShowing())是否成立,Y 如果成立,就1.獲取該廣播攜帶的數據(UIDfineAction.RESULT_KEY, 1),賦值給局部變量result,如果result==0,那么1. 保存"用戶名=密碼"到sp文件2. 保存用戶名到sp文件3. 將.properties文件的信息賦值給一個properties對象,給Config類的靜態成員變量賦值4. 如果當前Activity有LoginDialog對象,先置空,然后創建一個,并顯示出來。1、 在LoginDialog的構造函數中: 1. 加載UI1. 實例化View2. 給ListView設置數據適配器(===數據源來自Config類中保存的Properties文件中的client_id的值)
? ? 1. 在getView 中,在Item尾部復選框的點擊事件中,判斷當前item的位置是否在clients.size()的范圍內,
? ? 如果在,就將設置當前Item為選中狀態反置,并且根據當前Item是否被選中來確定是否給currentSelectClient賦值。
? ? 如果當前Item是選中狀態就將當前position+"",賦值給currentSelectClient,否則賦值""給它。
? ? 3. 綁定按鈕的點擊事件"對話框底部的--OK,立即體驗"的點擊事件
? ? 點擊事件邏輯:
? ? 1. 判斷listview的item是否有被選中過,
? ? Y 如果有:?
? ? 1. 將當前系統時間毫秒值賦值給成員變量mTime
? ? 2. 將sp中sp_CLIENT_ID字段賦值為""
? ? 3. 開啟一個定時器,30秒后發送Handler消息,攜帶what==0;
? ? 4. 彈出對話框顯示"正在登入賬號,請稍等..."
? ? 5. 發送廣播,action= UIDfineAction.ACTION_LOGIN,
? ? 并攜帶數據("cliend_id", Config.getClient_id().split(",")[Integer.parseInt(currentSelectClient)]),
? ? 以及("cliend_pwd", Config.getClient_token().split(",")[Integer.parseInt(currentSelectClient)]),
? ? 以及("sid", Config.getMain_account()),以及("sid_pwd", Config.getMain_token());
? ? N 如果沒有選中過Item:
? ? 1. 將sp中sp_CLIENT_ID字段賦值為""
? ? 2. 土司顯示"選擇一個測試用戶"
? ? ?
2. 否則(result!=0),根據result值的不同->土司顯示具體的失敗原因。
N 如果不成立,就不做任何處理.
2. action== UIDfineAction.ACTION_TCP_LOGIN_CLIENT_RESPONSE:{在application打開時就創建的Service中的連接失敗時的回調,以及連接成功時的回調會發送該action的廣播}
1. 判斷mLoginDialog是否還在顯示
Y 在顯示,判斷意圖對象攜帶的數據(UIDfineAction.RESULT_KEY, 1)==0?
Y ==0
1. 土司提醒登錄成功
2. 過一秒鐘,關閉對話框,跳轉到AbilityShowActivity界面,關閉當前Activity。
N !=0
1. 土司提醒登錄失敗+攜帶的數據值。
}
5. AudioActivity{AudioActivity的邏輯:1. onCreate方法的邏輯處理1. 實例化View控件2. 給ListView設置數據,數據來源-->Config類中的Properties中保存的。1. ListView的Item的點擊事件:1.通過意圖開啟新的Activity-->AudioCallActivity,并攜帶數據("call_client", call_client),以及("call_phone", phone),以及("call_position", phone_position);}
6. AudioCallActivity{AudioCallActivity的邏輯:1. onCreate方法的邏輯處理1. 實例化View控件2. 給View綁定點擊事件1.免費電話的點擊事件1. 檢測網絡連接,如果沒有網絡連接,就結束該方法2. 如果走到這一步,通過意圖啟動新的界面-->AudioConverseActivity并攜帶數據("call_client",getIntent().getStringExtra("call_client")),以及("call_type", 1);}
7.AudioConverseActivity{AudioConverseActivity的邏輯:1.onCreate方法的邏輯處理1. 初始化Views1. 實例化xml中定義的view 2. 給相應的View設置開啟當前Activity的intent對象攜帶的數據3. 給相應的View綁定點擊事件1. 靜音按鈕的點擊事件的邏輯處理:1. UI做相應的背景切換2. 通過SDK提供的UCSCall.setMicMute(boolean)來反置當前是否靜音的狀態2. 揚聲器按鈕的點擊事件的邏輯處理:1. UI做相應的背景切換2. 通過SDK提供的UCSCall.setSpeakerphone(boolean)來反置當前是否打開揚聲器的狀態3. 接通按鈕的點擊事件的邏輯處理:1. 停止響鈴(SDK提供的方法)UCSCall.stopRinging();2. 接聽UCSCall.answer("");3. 關閉揚聲器4. 掛掉按鈕的點擊事件的邏輯處理1. 停止響鈴2. 掛斷3. 1.5秒之后,關閉當前界面5. 結束通話按鈕的點擊事件的邏輯處理1. 停止響鈴2. 關閉揚聲器3. 掛掉4. 1.5秒之后,關閉當前界面6. 結束通話(鍵盤界面中的按鈕)的點擊邏輯處理:如同5的邏輯7. 打開鍵盤按鈕的點擊事件邏輯處理:1.key_layout 顯示2.converse_main 隱藏8. 關閉鍵盤按鈕的點擊事件的邏輯處理:7取反邏輯9. 各數字鍵點擊的邏輯處理1. 調用SDK提供的UCSCall.sendDTMF(context,str,view)去處理2. 獲取系統服務AudioManager3. 獲取系統音量最大值4. 獲取當前系統音量5. 關閉屏幕觸摸音6. 注冊廣播,接收7個廣播:接收的廣播的action: UIDfineAction.ACTION_DIAL_STATE&& UIDfineAction.ACTION_CALL_BACK&& UIDfineAction.ACTION_ANSWER&& UIDfineAction.ACTION_CALL_TIME&& UIDfineAction.ACTION_DIAL_HANGUP&& UIDfineAction.ACTION_NETWORK_STATE&& android.intent.action.HEADSET_PLUG // 插拔耳麥廣播7. 獲取當前Activity開啟時,傳遞的意圖攜帶的數據,并賦值給成員變量8. 根據意圖攜帶的("inCall", false)的值,來判斷當前是來電還是去電,分別做不同的邏輯處理Y,如果為true,說明是來電:1. 如果傳遞的意圖中攜帶了昵稱信息,就在UI中顯示昵稱,否則顯示電話號碼2. 打開揚聲器3. 開始響鈴4. 接聽,掛斷的View隱藏,結束通話的View顯示N,不是true,說明是去電:1. 接聽,結束通話View隱藏,掛斷View顯示2. 進行撥號1. 關閉揚聲器2. 打開屏幕觸摸音3. 創建一個新的意圖對象1. action==UIDfineAction.ACTION_DIAL2. 根據當前Activity的打開意圖是否攜帶UIDfineAction.FROM_NUM_KEY對應的Str如果有攜帶就將該數據封裝到剛創建的意圖中3. 根據當前Activity的打開意圖是否攜帶UIDfineAction.TO_NUM_KEY對應的Str如果有攜帶就將該數據封裝到剛創建的意圖中4. 根據callType(當前Activity打開意圖攜帶過來的)的不同,再封裝兩組數據進剛才創建的意圖中,并發送該action對應的廣播,[action== UIDfineAction.ACTION_DIAL]case 0:// 直撥傳入電話號碼sendBroadcast(intent.putExtra(UIDfineAction.CALL_PHONE, calledPhone).putExtra("type", calltype));break;case 1:// 免費傳入clientidsendBroadcast(intent.putExtra(UIDfineAction.CALL_UID, calledUid).putExtra("type", calltype));break;case 2:// 回撥傳入電話號碼sendBroadcast(intent.putExtra(UIDfineAction.CALL_PHONE, calledPhone).putExtra("type", calltype));break;case 3:// 視頻點對點 傳入clientidsendBroadcast(intent.putExtra(UIDfineAction.CALL_UID, calledPhone).putExtra("type", calltype));break;case 5:// 智能撥打 clientid和電話號碼同時傳入,先選擇clientid進行免費撥打,如果對方不在線再選擇手機號碼進行直撥sendBroadcast(intent.putExtra(UIDfineAction.CALL_UID, calledUid).putExtra(UIDfineAction.CALL_PHONE, calledPhone).putExtra("type", calltype));---->該廣播在app打開時創建的廣播中有處理!!!3. 根據intent傳入的callType的不同,分別在UI上顯示"免費電話呼叫中","直撥電話呼叫中"等不同文字2. 廣播接收器中的邏輯處理1. action== UIDfineAction.ACTION_DIAL_STATE 1. 根據意圖傳遞的state的值給TextView.converse_information顯示不同的文字(多半是錯誤信息)。2. action== UIDfineAction.ACTION_CALL_BACK1. converse_information.setText("回撥成功");2. 停止響鈴(來去電的響鈴都停止)3. 1.5秒之后關閉當前界面3. action== UIDfineAction.ACTION_ANSWER1. UI做相應的改變2. 停止響鈴3. 關閉揚聲器4. 變量open_headset = true;4. action== UIDfineAction.ACTION_DIAL_HANGUP1. 1.5秒之后,關閉當前界面5. action== UIDfineAction.ACTION_CALL_TIME1. 獲取intent攜帶的數據String timer = intent.getStringExtra("timer");2. 將獲取的數據賦值給converse_information.setText(timer);6. action== UIDfineAction.ACTION_NETWORK_STATE1. 獲取Intent攜帶的數據("state", -1),并根據該數值在TextView中顯示網絡狀態極好,還是一般還是差等信息。7. action== "android.intent.action.HEADSET_PLUG" //插拔耳麥廣播1. 根據當前是否有耳麥插入,如果耳麥插入就關閉揚聲器,否則打開。3. onDestory的邏輯處理1.反注冊廣播2.停止響鈴3.如果之前有關閉過屏幕觸摸音,現在打開} *******
> 第一條線,也就是語音通話的整個流程差不多就這樣子了。不過,由于水平有限,以及無意的疏漏,還是有可能對源碼的理解有些偏差與遺漏。不過,大體流程,也就是這樣子了。
>
> 簡單總結一下語音通話這條線的邏輯,差不多就是> 1. 將必要的數據保存到文件中2. 在需要通知其他界面或者后臺做對應操作的時候,通過這邊發送廣播,那邊接收廣播的方式進行數據的傳遞。
>
>這條線的一個重要的類就是`Config`類,這個`Config`類其實算是`java.util.Properties`的一個包裝類,所有的核心邏輯就是將數據存入`/config.properties`文件中,以及,從該`.properties`文件中取出對應的數據,在各個需要的UI進行數據展示。>首先,在首次登錄的時候,會發一個post請求將登錄信息發送到云之訊的服務器端,返回一個json串。然后就調用了`Config.parseConfig(json,context)`的方法,將json中返回的登錄信息,帳號信息等存放到了`.properties`文件中,然后后面的操作都是基于這個配置文件的中的數據來的。
2. 給ListView設置數據適配器(<===數據源來自Config類中保存的Properties文件中的client_id的值)
? ? 1. 在getView 中,在Item尾部復選框的點擊事件中,判斷當前item的位置是否在clients.size()的范圍內,
? ? 如果在,就將設置當前Item為選中狀態反置,并且根據當前Item是否被選中來確定是否給currentSelectClient賦值。
? ? 如果當前Item是選中狀態就將當前position+"",賦值給currentSelectClient,否則賦值""給它。
? ? 3. 綁定按鈕的點擊事件"對話框底部的--OK,立即體驗"的點擊事件
? ? 點擊事件邏輯:
? ? 1. 判斷listview的item是否有被選中過,
? ? Y 如果有:?
? ? 1. 將當前系統時間毫秒值賦值給成員變量mTime
? ? 2. 將sp中sp_CLIENT_ID字段賦值為""
? ? 3. 開啟一個定時器,30秒后發送Handler消息,攜帶what==0;
? ? 4. 彈出對話框顯示"正在登入賬號,請稍等..."
? ? 5. 發送廣播,action= UIDfineAction.ACTION_LOGIN,
? ? 并攜帶數據("cliend_id", Config.getClient_id().split(",")[Integer.parseInt(currentSelectClient)]),
? ? 以及("cliend_pwd", Config.getClient_token().split(",")[Integer.parseInt(currentSelectClient)]),
? ? 以及("sid", Config.getMain_account()),以及("sid_pwd", Config.getMain_token());
? ? N 如果沒有選中過Item:
? ? 1. 將sp中sp_CLIENT_ID字段賦值為""
? ? 2. 土司顯示"選擇一個測試用戶"
? ? ?
2. 否則(result!=0),根據result值的不同->土司顯示具體的失敗原因。
N 如果不成立,就不做任何處理.
2. action== UIDfineAction.ACTION_TCP_LOGIN_CLIENT_RESPONSE:{在application打開時就創建的Service中的連接失敗時的回調,以及連接成功時的回調會發送該action的廣播}
1. 判斷mLoginDialog是否還在顯示
Y 在顯示,判斷意圖對象攜帶的數據(UIDfineAction.RESULT_KEY, 1)==0?
Y ==0
1. 土司提醒登錄成功
2. 過一秒鐘,關閉對話框,跳轉到AbilityShowActivity界面,關閉當前Activity。
N !=0
1. 土司提醒登錄失敗+攜帶的數據值。
}
總結
以上是生活随笔為你收集整理的云之讯官方测试Demo音频版源码阅读(编辑)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一款小巧好用的全局鼠标手势软件——Mou
- 下一篇: java合成图片并添加文字