Android即時(shí)通訊設(shè)計(jì)(騰訊IM接入和WebSocket接入)
一、前言
之前項(xiàng)目的群聊是用數(shù)據(jù)庫直接操作的,體驗(yàn)很差,消息很難即時(shí)反饋,所以最后考慮到了使用騰訊的IM完成群聊的接入,不過中途還是有點(diǎn)小坎坷的,接入完成之后發(fā)現(xiàn)體驗(yàn)版一個(gè)群聊只有20人,當(dāng)時(shí)看到體驗(yàn)版支持100個(gè)用戶也就忍了,現(xiàn)在一個(gè)群聊只能20用戶,忍不了了,所以暫時(shí)找到了WebSocket作為臨時(shí)的解決方案(等有錢了再換),同時(shí)支持50個(gè)用戶在線聊天,也算還行,勉強(qiáng)夠用,下面就介紹兩種實(shí)現(xiàn)方案的接入
二、騰訊IM接入
騰訊云IM的官網(wǎng),這里的接入將其中群聊相關(guān)的api抽取出來,更多請(qǐng)看文檔(如果有時(shí)間的話,完全可以實(shí)現(xiàn)一個(gè)類似QQ的簡單聊天平臺(tái))
1.準(zhǔn)備工作
需求分析
需要實(shí)現(xiàn)一個(gè)類似QQ中群聊的功能,只需要開發(fā)簡單的接收消息,發(fā)送消息,獲取歷史記錄這三個(gè)簡單的功能即可
創(chuàng)建應(yīng)用
這部分就不演示了,很簡單,創(chuàng)建好大概是下圖的樣子
體驗(yàn)版可以支持100個(gè)用戶和一個(gè)群聊20個(gè)用戶,提供免費(fèi)的云存儲(chǔ)7天,同時(shí)可以創(chuàng)建多個(gè)IM實(shí)例,如果是學(xué)習(xí)使用的話體驗(yàn)版足夠了,商業(yè)化考慮專業(yè)版和旗艦版
依賴集成
使用gradle集成,也可以使用sdk集成,這里采用新版的sdk進(jìn)行集成
api 'com.tencent.imsdk:imsdk-plus:6.1.2155'
2.初始化工作
初始化IM
創(chuàng)建實(shí)例
參數(shù)中有一個(gè)回調(diào),這里的object相當(dāng)于java里面的匿名類
val config = V2TIMSDKConfig() V2TIMManager.getInstance() .initSDK(this, sdkId, config, object : V2TIMSDKListener() { override fun onConnecting() { // 正在連接到騰訊云服務(wù)器 Log.e("im", "正在連接到騰訊云服務(wù)器") } override fun onConnectSuccess() { // 已經(jīng)成功連接到騰訊云服務(wù)器 Log.e("im", "已經(jīng)成功連接到騰訊云服務(wù)器") } override fun onConnectFailed(code: Int, error: String) { // 連接騰訊云服務(wù)器失敗 Log.e("im", "連接騰訊云服務(wù)器失敗") } })
生成登錄憑據(jù)
這部分官方提供客戶端快速生成的代碼和服務(wù)端代碼,具體可以到官網(wǎng)找找,一開始測試的時(shí)候可以考慮客戶端代碼后面正式的項(xiàng)目最好部署到服務(wù)端進(jìn)行處理,這部分就提個(gè)醒,服務(wù)端有兩個(gè)文件,當(dāng)時(shí)沒看清楚,找了好久的函數(shù),最后發(fā)現(xiàn)是某個(gè)java文件忘記看了,還是同一級(jí)目錄下,應(yīng)該是其他api也復(fù)用了Base64URL這個(gè)類
同時(shí)官方還提供生成和校驗(yàn)憑據(jù)的工具
用戶登錄
這部分只需要傳入?yún)?shù)即可
V2TIMManager.getInstance().login(currentUser,sig, object : V2TIMCallback { override fun onSuccess() { Log.e("im", "${currentUser}登錄成功") } override fun onError(code: Int, desc: String?) { Log.e("im", "${currentUser}登錄失敗,錯(cuò)誤碼為:$[code],具體錯(cuò)誤:${desc}") } })
- currentUser 即用戶的id
- sig 即用戶的登錄憑據(jù)
- V2TIMCallback 回調(diào)的一個(gè)類
3.群聊相關(guān)
創(chuàng)建群聊
創(chuàng)建群聊的時(shí)候需要注意幾個(gè)方面的問題
群聊類別(groupType)
需要審批還是不需要,最大的容納用戶數(shù),未支不支持未入群查看群聊消息,詳見下圖
其中社群其實(shí)挺符合我的需求的,但有個(gè)問題,社群需要付費(fèi)才能開通(還挺貴),所以最后選擇了Meeting類型的群組
群聊資料設(shè)置
群聊id(groupID)是沒有字母數(shù)字和特殊符號(hào)(當(dāng)然不能中文)都是可以的,群聊名字(groupName),群聊介紹(introduction)等等,還有就是設(shè)置初始的成員,可以將主管理員加入(這里稍微有點(diǎn)疑惑的就是創(chuàng)建群聊,居然沒有默認(rèn)添加創(chuàng)建人)
創(chuàng)建群聊的監(jiān)聽回調(diào)
這里傳入的參數(shù)就是上述的groupInfo和memberInfoList,主要用于初始化群聊,然后有一個(gè)回調(diào)的參數(shù)監(jiān)聽創(chuàng)建結(jié)果
val group = V2TIMGroupInfo() group.groupName = "test" group.groupType = "Meeting" group.introduction = "more to show" group.groupID = "test" val memberInfoList: MutableList<V2TIMCreateGroupMemberInfo> = ArrayList() val memberA = V2TIMCreateGroupMemberInfo() memberA.setUserID("master") memberInfoList.add(memberA) V2TIMManager.getGroupManager().createGroup( group, memberInfoList, object : V2TIMValueCallback<String?> { override fun onError(code: Int, desc: String) { // 創(chuàng)建失敗 Log.e("im","創(chuàng)建失敗$[code],詳情:${desc}") } override fun onSuccess(groupID: String?) { // 創(chuàng)建成功 Log.e("im","創(chuàng)建成功,群號(hào)為${groupID}") } })
加入群聊
這部分只需要一個(gè)回調(diào)監(jiān)聽即可,這里沒有login的用戶的原因是,默認(rèn)使用當(dāng)前登錄的id加群,所以一個(gè)很重要的前提是登錄
V2TIMManager.getInstance().joinGroup("群聊ID","驗(yàn)證消息",object :V2TIMCallback{ override fun onSuccess() { Log.e("im","加群成功") } override fun onError(p0: Int, p1: String?) { Log.e("im","加群失敗") } })
4.消息收發(fā)相關(guān)
發(fā)送消息
這里發(fā)送消息是采用高級(jí)接口,發(fā)送的消息類型比較豐富,并且支持自定義消息類型,所以這里采用了高級(jí)消息收發(fā)接口
首先創(chuàng)建消息,這里是創(chuàng)建自定義消息,其他消息同理
val myMessage = "一段自定義的json數(shù)據(jù)" //由于這里自定義消息接收的參數(shù)為byteArray類型的,所以進(jìn)行一個(gè)轉(zhuǎn)換 val messageCus= V2TIMManager.getMessageManager().createCustomMessage(myMessage.toByteArray())
發(fā)送消息,這里需要設(shè)置一些參數(shù)
messageCus即轉(zhuǎn)換過后的byte類型的數(shù)據(jù),toUserId即接收方,這里為群聊的話,用空字符串置空即可,groupId即群聊的ID,如果是單聊的話,這里同樣置空字符串即可,weight即你的消息被接收到的權(quán)重(不保證全部都能收到,這里設(shè)置權(quán)重確定優(yōu)先級(jí)),onlineUserOnly即是否只有在線的用戶可以收到,這個(gè)的話設(shè)置false即可,offlinePushInfo這個(gè)只有旗艦版才有推送消息的功能,所以這里設(shè)置null即可,然后就是一個(gè)發(fā)送消息的回調(diào)
V2TIMManager.getMessageManager().sendMessage(messageCus,toUserId,groupId,weight,onlineUserOnly, offlinePushInfo,object:V2TIMSendCallback<V2TIMMessage>{ override fun onSuccess(message: V2TIMMessage?) { Log.e("im","發(fā)送成功,內(nèi)容為:${message?.customElem}") //這里同時(shí)需要自己進(jìn)行解析消息,需要轉(zhuǎn)換成String類型的數(shù)據(jù) val data = String(message?.customElem?.data) ... } override fun onError(p0: Int, p1: String?) { Log.e("im","錯(cuò)誤碼為:${p0},具體錯(cuò)誤:${p1}") } override fun onProgress(p0: Int) { Log.e("im","處理進(jìn)度:${p0}") } })
獲取歷史消息
- groupId即群聊ID
- pullNumber即拉取消息數(shù)量
- lastMessage即上一次的消息,用于獲取更多消息的定位
- V2TIMValueCallback即消息回調(diào)
這里關(guān)于lastMessage進(jìn)行解釋說明,這個(gè)參數(shù)可以設(shè)置成全局變量,然后一開始設(shè)置為null,然后獲取到的消息列表的最后一條設(shè)置成lastMessage即可
V2TIMManager.getMessageManager().getGroupHistoryMessageList( groupId,pullNumber,lastMessage,object:V2TIMValueCallback<List<V2TIMMessage>>{ override fun onSuccess(p0: List<V2TIMMessage>?) { if (p0 != null) { if (p0.isEmpty()){ Log.e("im","沒有更多消息了") "沒有更多消息了".showToast() }else { //記錄最后一條消息 lastMessage = p0[p0.size - 1] for (msgIndex in p0.indices) { //解析各種消息 when(p0[msgIndex].elemType){ V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{ ... } V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {} ... } else -> { ... } } } } } } override fun onError(p0: Int, p1: String?) { .... } })
新消息的監(jiān)聽
這個(gè)主要用于新消息的接收和監(jiān)聽,同時(shí)需要自己對(duì)于各種消息的解析和相關(guān)處理
V2TIMManager.getMessageManager().addAdvancedMsgListener(object:V2TIMAdvancedMsgListener(){ override fun onRecvNewMessage(msg: V2TIMMessage?) { Log.e("im","新消息${msg?.customElem}") //這里針對(duì)多種消息類型有不同的處理方法 when(msg?.elemType){ V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{ val message = msg.customElem?.data ... } V2TIMMessage.V2TIM_ELEM_TYPE_TEXT ->{ val message = msg.textElem.text ... } else -> { "暫時(shí)不支持此消息的接收".showToast() Log.e("im","${msg?.elemType}") } } } })
至此接入部分就已經(jīng)完成了,這里只是簡單的介紹接入,還有更多的細(xì)節(jié)可以查看項(xiàng)目源碼
三、WebSocket接入
這個(gè)需求和上面的是一樣的,同時(shí)提供和上面騰訊IM類似功能的api,這部分涉及網(wǎng)絡(luò)相關(guān)的api(不是非常專業(yè)),主要描述一些思路上的,具體代碼不是很困難
1.WebSocket介紹
webSocket可以實(shí)現(xiàn)長連接,可以作為消息接收的即時(shí)處理的一個(gè)工具,采用ws協(xié)議或者wss協(xié)議(SSL)進(jìn)行通信,騰訊IM的版本也推出了webSocket實(shí)現(xiàn)方案,webSocket主要解決的痛點(diǎn)就是服務(wù)端不能主動(dòng)推送消息,代替之前輪詢的實(shí)現(xiàn)方案
2.服務(wù)端相關(guān)
服務(wù)端采用springboot進(jìn)行開發(fā),同時(shí)也是使用kotlin進(jìn)行編程
webSoket 依賴集成
下面是gradle的依賴集成
implementation "org.springframework.boot:spring-boot-starter-websocket"
WebSocketConfig配置相關(guān)
@Configuration class WebSocketConfig { @Bean fun serverEndpointExporter(): ServerEndpointExporter { return ServerEndpointExporter() } }
WebSocketServer相關(guān)
這部分代碼是關(guān)鍵代碼,里面重寫了webSocket的四個(gè)方法,然后配置靜態(tài)的變量和方法用于全局通信,下面給出一個(gè)框架
@ServerEndpoint("/imserver/{userId}") @Component class WebSocketServer { @OnOpen fun onOpen(session: Session?, @PathParam("userId") userId: String) { ... } @OnClose fun onClose() { ... } @OnMessage fun onMessage(message: String, session: Session?) { ... } @OnError fun onError(session: Session?, error: Throwable) { ... } //主要解決@Component和@Resource沖突導(dǎo)致未能自動(dòng)初始化的問題 @Resource fun setMapper(chatMapper: chatMapper){ WebSocketServer.chatMapper = chatMapper } //這是發(fā)送消息用到的函數(shù) @Throws(IOException::class) fun sendMessage(message: String?) { session!!.basicRemote.sendText(message) } //靜態(tài)變量和方法 companion object { ... } }
companion object
這里一個(gè)比較關(guān)鍵的變量就是webSocketMap存儲(chǔ)用戶的webSocket對(duì)象,后面將利用這個(gè)實(shí)現(xiàn)消息全員推送和部分推送
companion object { //統(tǒng)計(jì)在線人數(shù) private var onlineCount: Int = 0 //用于存放每個(gè)用戶對(duì)應(yīng)的webSocket對(duì)象 val webSocketMap = ConcurrentHashMap<String, WebSocketServer>() //操作數(shù)據(jù)庫的mapper對(duì)象的延遲初始化 lateinit var chatMapper:chatMapper //服務(wù)端主動(dòng)推送消息的對(duì)外開放的方法 @Throws(IOException::class) fun sendInfo(message: String, @PathParam("userId") userId: String) { if (userId.isNotBlank() && webSocketMap.containsKey(userId)) { webSocketMap[userId]?.sendMessage(message) } else { println("用戶$userId,不在線!") } } //在線統(tǒng)計(jì) @Synchronized fun addOnlineCount() { onlineCount++ } //離線統(tǒng)計(jì) @Synchronized fun subOnlineCount() { onlineCount-- } }
@OnOpen
這個(gè)方法在websocket打開時(shí)執(zhí)行,主要執(zhí)行一些初始化和統(tǒng)計(jì)工作
@OnOpen fun onOpen(session: Session?, @PathParam("userId") userId: String) { this.session = session this.userId = userId if (webSocketMap.containsKey(userId)) { //包含此id說明此時(shí)其他地方開啟了一個(gè)webSocket通道,直接kick下線重新連接 webSocketMap.remove(userId) webSocketMap[userId] = this } else { webSocketMap[userId] = this addOnlineCount() } println("用戶連接:$userId,當(dāng)前在線人數(shù)為:$onlineCount") }
@OnClose
這個(gè)方法在webSocket通道結(jié)束時(shí)調(diào)用,執(zhí)行下線邏輯和相關(guān)的統(tǒng)計(jì)工作
@OnClose fun onClose() { if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId) subOnlineCount() } println("用戶退出:$userId,當(dāng)前在線人數(shù)為:$onlineCount") }
@OnMessage
這個(gè)方法用于處理消息分發(fā),這里一般需要對(duì)消息進(jìn)行一些處理,具體處理參考自定義消息的處理,這里是設(shè)計(jì)成群聊的方案,所以采用
@OnMessage fun onMessage(message: String, session: Session?) { if (message.isNotBlank()) { //解析發(fā)送的報(bào)文 val newMessage = ... //這里需要進(jìn)行插入一條數(shù)據(jù),做持久化處理,即未在線的用戶也同樣可以看到這條消息 chatMapper.insert(newMessage) //遍歷所有的消息 webSocketMap.forEach { it.value.sendMessage(sendMessage.toMyJson()) } } }
@OnError
發(fā)生錯(cuò)誤調(diào)用的方法
@OnError fun onError(session: Session?, error: Throwable) { println("用戶錯(cuò)誤:$userId 原因: ${error.message}") error.printStackTrace() }
sendMessage
此方法用于消息分發(fā)給各個(gè)客戶端時(shí)調(diào)用的
fun sendMessage(message: String?) { session!!.basicRemote.sendText(message) }
WebSocketController
這部分主要是實(shí)現(xiàn)服務(wù)端直接推送消息設(shè)計(jì)的,類似系統(tǒng)消息的設(shè)定
@PostMapping("/sendAll/{message}") fun sendAll(@PathVariable message: String):String{ //消息的處理 val newMessage = ... //需不要存儲(chǔ)系統(tǒng)消息就看具體需求了 WebSocketServer.webSocketMap.forEach { WebSocketServer.sendInfo(sendMessage.toMyJson(), it.key) } return "ok" } @PostMapping("/sendC2C/{userId}/{message}") fun sendC2C(@PathVariable userId:String,@PathVariable message:String):String{ //消息的處理 val newMessage = ... WebSocketServer.sendInfo(newMessage, userId) return "ok" }
至此服務(wù)端的講解就結(jié)束了,下面就看看我們安卓客戶端的實(shí)現(xiàn)了
3.客戶端相關(guān)
依賴集成
集成java語言的webSocket(四舍五入就是Kotlin版本的)
implementation 'org.java-websocket:Java-WebSocket:1.5.2'
實(shí)現(xiàn)部分
這部分的重寫的方法和服務(wù)端差不多,但少了服務(wù)相關(guān)的處理,代碼少了很多,這里需要提醒的一點(diǎn)就是,重寫的這些方法都是子線程中運(yùn)行的,不允許直接寫入U(xiǎn)I相關(guān)的操作,所以這里需要使用handle進(jìn)行處理或者使用runOnUIThread
val userSocket = object :WebSocketClient(URI("wss://服務(wù)端地址:端口號(hào)/imserver/${userId}")){ override fun onOpen(handshakedata: ServerHandshake?) { //打開進(jìn)行初始化的操作 } override fun onMessage(message: String?) { ... //這里做recyclerView的更新 } override fun onClose(code: Int, reason: String?, remote: Boolean) { //這里執(zhí)行一個(gè)通知操作即可 ... } override fun onError(ex: Exception?) { ... } } userSocket.connect() //斷開連接的話使用自帶的reconnect重新連接即可 //需要注意的一點(diǎn)就是不能在重寫方法里面執(zhí)行這個(gè)操作 userSocket.reconnect()
這里還有太多很多細(xì)節(jié)不能一一展示,但就總體而言是模仿上述騰訊IM實(shí)現(xiàn)的,具體的可以看項(xiàng)目地址
四、列表設(shè)計(jì)的一些細(xì)節(jié)
這里簡單敘述一下列表設(shè)計(jì)的一些細(xì)節(jié),這部分設(shè)計(jì)還是挺繁瑣的
1.handle的使用
列表的更新時(shí)間和時(shí)機(jī)是取決于具體網(wǎng)絡(luò)獲取情況的,故需要一個(gè)全局的handle用于處理其中的消息,同時(shí)列表滑動(dòng)行為不一樣,這里需要注意的一個(gè)小問題,就是message最好是用一個(gè)發(fā)一個(gè),不然可能出現(xiàn)內(nèi)存泄漏的風(fēng)險(xiǎn)
- 下拉刷新,此時(shí)刷新完畢列表肯定就是在第一個(gè)item的位置不然就有點(diǎn)奇怪
- 首次獲取歷史消息,此時(shí)的場景應(yīng)該是列表最后一個(gè)item
- 獲取新消息,也是最后一個(gè)item
private val up = 1 private val down = 2 private val fail = 0 private val handler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: android.os.Message) { when (msg.what) { up -> { viewBinding.chatRecyclerview.scrollToPosition(0) viewBinding.swipeRefresh.isRefreshing = false } down ->{ viewBinding.chatRecyclerview.scrollToPosition(viewModel.chatList.size-1) } fail -> { "刷新失敗請(qǐng)檢查網(wǎng)絡(luò)".showToast() viewBinding.swipeRefresh.isRefreshing = false } } } }
2.消息的獲取和RecycleView的刷新
消息部分設(shè)計(jì)成從新到老的設(shè)計(jì),上述騰訊IM也是這個(gè)順序,所以這部分添加列表時(shí)需要加在最前面
viewModel.chatList.add(0,msg) adapter.notifyItemInserted(0)
同時(shí)需要注意的就是刷新位置,這部分是插入故使用adapter中響應(yīng)的notifyItemInserted方法進(jìn)行提醒列表刷新,雖然直接使用最通用的notifyDataSetChanged也是可以達(dá)到相同的目的,但體驗(yàn)效果就不那么好了,如果是大量的數(shù)據(jù),可能會(huì)產(chǎn)生比較大的延遲
3.關(guān)于消息item的設(shè)計(jì)細(xì)節(jié)
這個(gè)item具體是模仿QQ的布局進(jìn)行設(shè)計(jì)的,這里底色部分沒有做調(diào)整
可以優(yōu)化的更好的部分就是時(shí)間,可以對(duì)列表時(shí)間進(jìn)行判斷,然后實(shí)現(xiàn)類似昨天,前天等等的相對(duì)時(shí)間,這里使用的是constraintlayout和linearlayout的嵌套使用,這里當(dāng)時(shí)遇到一個(gè)問題即文字需要自適應(yīng)列表,如果沒有另外嵌套一個(gè)布局就會(huì)導(dǎo)致wrap_content的填充方式可能會(huì)超出界面,出現(xiàn)半個(gè)字的情況,猜測wrap_content最大的寬度是根布局的寬度導(dǎo)致的,所以最后嵌套了一個(gè)布局解決了,下面是設(shè)計(jì)的框架圖
五、項(xiàng)目使用的接口和地址
web項(xiàng)目比較復(fù)雜,是在之前的基礎(chǔ)上開發(fā)的,獨(dú)立抽離出來有點(diǎn)困難,所以這里就不放web端的代碼,這里提供客戶端的代碼,只需要替換自己的sdkId和服務(wù)端相關(guān)的url即可運(yùn)行,同時(shí)這里涉及一些與服務(wù)端有關(guān)的交互,這里簡單介紹一下服務(wù)端需要開發(fā)的接口
獲取歷史數(shù)據(jù)的接口
這里兩個(gè)參數(shù),一個(gè)確定拉取消息數(shù)目,一個(gè)確定拉取起始時(shí)間點(diǎn)
//獲取聊天記錄 @GET("chat/refreshes/{time}/{number}") fun getChat(@Path("time")time:String, @Path("number")count:Int): Call<MessageResponse>
獲取騰訊IM的user簽名
//生成應(yīng)用憑據(jù) @GET("imSig/{userId}/{expire}") fun getSig(@Path("userId")userId:String,@Path("expire")expire:Long):Call<String>
還有兩個(gè)推送使用的接口,在前面已經(jīng)敘述過了
項(xiàng)目地址:https://github.com/xyh-fu/ImTest.git
六、總結(jié)
這次IM即時(shí)通訊的設(shè)計(jì)收獲滿滿,get到一個(gè)新的知識(shí)點(diǎn)也算還行(主要是貧窮限制的),后期可以考慮全部換成騰訊的IM,畢竟自己實(shí)現(xiàn)的只是小規(guī)模測試和商業(yè)產(chǎn)品還是有很大的區(qū)別。服務(wù)端涉及的稍微多一點(diǎn)點(diǎn),客戶端是比較簡單,比較麻煩的就是消息處理機(jī)制,考慮到設(shè)計(jì)的接口各異,還有服務(wù)端的數(shù)據(jù)庫等等,難以統(tǒng)一,故不一一展開敘述。
到此這篇關(guān)于Android即時(shí)通訊設(shè)計(jì)(騰訊IM接入和WebSocket接入)的文章就介紹到這了,更多相關(guān)Android即時(shí)通訊設(shè)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android編程實(shí)現(xiàn)定時(shí)發(fā)短信功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)定時(shí)發(fā)短信功能,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android定時(shí)發(fā)送短信功能的相關(guān)原理、實(shí)現(xiàn)方法與注意事項(xiàng),需要的朋友可以參考下2017-09-09Android封裝實(shí)現(xiàn)短信驗(yàn)證碼的獲取倒計(jì)時(shí)
這篇文章主要介紹了Android封裝實(shí)現(xiàn)短信驗(yàn)證碼的獲取倒計(jì)時(shí),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-03-03使用adb?or?fastboot命令進(jìn)入高通的9008(edl)模式的兩種方法
這篇文章主要介紹了使用adb?or?fastboot命令進(jìn)入高通的9008(edl)模式,兩種方式通過命令給大家寫的非常詳細(xì),文中又給大家補(bǔ)充介紹了高通手機(jī)?進(jìn)入?高通9008模式的兩種方法,需要的朋友可以參考下2023-01-01Android?startActivityForResult的調(diào)用與封裝詳解
startActivityForResult?可以說是我們常用的一種操作了,目前有哪些方式實(shí)現(xiàn)?startActivityForResult?的功能呢?本文就來和大家詳細(xì)聊聊2023-03-03Android SharedPreferences存儲(chǔ)的正確寫法
這篇文章主要介紹了Android SharedPreferences存儲(chǔ)的正確寫法的相關(guān)資料,需要的朋友可以參考下2017-06-06Android實(shí)現(xiàn)網(wǎng)易嚴(yán)選標(biāo)簽欄滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)網(wǎng)易嚴(yán)選標(biāo)簽欄滑動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android圖片異步加載框架Android-Universal-Image-Loader
這篇文章主要介紹了Android圖片異步加載框架Android-Universal-Image-Loader,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05Android實(shí)現(xiàn)郵箱驗(yàn)證功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)郵箱驗(yàn)證功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05Android自定義VIew實(shí)現(xiàn)衛(wèi)星菜單效果淺析
這篇文章主要介紹了Android自定義VIew實(shí)現(xiàn)衛(wèi)星菜單效果淺析,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11