Android實(shí)現(xiàn)兩臺(tái)手機(jī)屏幕共享和遠(yuǎn)程控制功能
一、項(xiàng)目概述
在遠(yuǎn)程協(xié)助、在線教學(xué)、技術(shù)支持等多種場(chǎng)景下,實(shí)時(shí)獲得另一部移動(dòng)設(shè)備的屏幕畫(huà)面,并對(duì)其進(jìn)行操作,具有極高的應(yīng)用價(jià)值。本項(xiàng)目旨在實(shí)現(xiàn)兩臺(tái) Android 手機(jī)之間的屏幕共享與遠(yuǎn)程控制,其核心功能包括:
主控端(Controller):捕獲自身屏幕并將實(shí)時(shí)畫(huà)面編碼后通過(guò)網(wǎng)絡(luò)發(fā)送;同時(shí)監(jiān)聽(tīng)用戶(hù)在主控端的觸摸、滑動(dòng)和按鍵等輸入操作,并將操作事件發(fā)送至受控端。
受控端(Receiver):接收屏幕畫(huà)面數(shù)據(jù)并實(shí)時(shí)解碼、渲染到本地界面;接收并解析主控端的輸入操作事件,通過(guò)系統(tǒng)接口模擬觸摸和按鍵,實(shí)現(xiàn)被控設(shè)備的操作。
通過(guò)這一方案,用戶(hù)可以實(shí)時(shí)“看到”受控端的屏幕,并在主控端進(jìn)行點(diǎn)觸、滑動(dòng)等交互,達(dá)到“遠(yuǎn)程操控”他機(jī)的效果。本項(xiàng)目的核心難點(diǎn)在于如何保證圖像數(shù)據(jù)的實(shí)時(shí)性與清晰度,以及如何準(zhǔn)確、及時(shí)地模擬輸入事件。
二、相關(guān)知識(shí)
2.1 MediaProjection API
概述:Android 5.0(API 21)引入的屏幕錄制和投影接口。通過(guò)
MediaProjectionManager
獲取用戶(hù)授權(quán)后,可創(chuàng)建VirtualDisplay
,將屏幕內(nèi)容輸送至Surface
或ImageReader
。關(guān)鍵類(lèi):
MediaProjectionManager
:請(qǐng)求屏幕捕獲權(quán)限MediaProjection
:執(zhí)行屏幕捕獲VirtualDisplay
:虛擬顯示、輸出到Surface
ImageReader
:以Image
幀的方式獲取屏幕圖像
2.2 Socket 網(wǎng)絡(luò)通信
概述:基于 TCP 協(xié)議的雙向流式通信,適合大塊數(shù)據(jù)的穩(wěn)定傳輸。
關(guān)鍵類(lèi):
ServerSocket
/Socket
:服務(wù)端監(jiān)聽(tīng)與客戶(hù)端連接InputStream
/OutputStream
:數(shù)據(jù)讀寫(xiě)
注意:需要設(shè)計(jì)簡(jiǎn)單高效的協(xié)議,在發(fā)送每幀圖像前加上幀頭(如長(zhǎng)度信息),以便接收端正確分包、組幀。
2.3 輸入事件模擬
概述:在非系統(tǒng)應(yīng)用中無(wú)法直接使用
InputManager
注入事件,需要借助無(wú)障礙服務(wù)(AccessibilityService)或系統(tǒng)簽名權(quán)限。關(guān)鍵技術(shù):
無(wú)障礙服務(wù)(AccessibilityService)注入觸摸事件
使用
GestureDescription
構(gòu)造手勢(shì)并通過(guò)dispatchGesture
觸發(fā)
2.4 數(shù)據(jù)壓縮與傳輸優(yōu)化
圖像編碼:將
Image
幀轉(zhuǎn)為 JPEG 或 H.264,以減小帶寬占用。數(shù)據(jù)分片:對(duì)大幀進(jìn)行分片發(fā)送,防止單次寫(xiě)入阻塞或觸發(fā)
OutOfMemoryError
。網(wǎng)絡(luò)緩沖與重傳:TCP 本身提供重傳,但需控制合適的發(fā)送速率,防止擁塞。
2.5 多線程與異步處理
概述:屏幕捕獲與網(wǎng)絡(luò)傳輸耗時(shí),需放在獨(dú)立線程或
HandlerThread
中,否則 UI 會(huì)卡頓。框架:
ThreadPoolExecutor
管理捕獲、編碼、發(fā)送任務(wù)HandlerThread
配合Handler
處理 IO 回調(diào)
三、實(shí)現(xiàn)思路
3.1 架構(gòu)設(shè)計(jì)
+--------------+ +--------------+ | |--(請(qǐng)求授權(quán))------------------->| | | MainActivity | | RemoteActivity| | |<-(啟動(dòng)服務(wù)、連接成功)-----------| | +------+-------+ +------+-------+ | | | 捕獲屏幕 -> MediaProjection -> ImageReader | 接收畫(huà)面 -> 解碼 -> SurfaceView | 編碼(JPEG/H.264) | | 發(fā)送 -> Socket OutputStream | | | 接收事件 -> 無(wú)障礙 Service -> dispatchGesture |<--觸摸事件包------------------------------------| | 模擬觸摸 => AccessibilityService | +------+-------+ +------+-------+ | ScreenShare | | RemoteControl| | Service | | Service | +--------------+ +--------------+
3.2 協(xié)議與數(shù)據(jù)格式
幀頭結(jié)構(gòu)(12 字節(jié))
4 字節(jié):幀類(lèi)型(0x01 表示圖像,0x02 表示觸摸事件)
4 字節(jié):數(shù)據(jù)長(zhǎng)度 N(網(wǎng)絡(luò)字節(jié)序)
4 字節(jié):時(shí)間戳(毫秒)
圖像幀數(shù)據(jù):
[幀頭][JPEG 數(shù)據(jù)]
觸摸事件數(shù)據(jù):
1 字節(jié):事件類(lèi)型(0:DOWN,1:MOVE,2:UP)
4 字節(jié):X 坐標(biāo)(float)
4 字節(jié):Y 坐標(biāo)(float)
8 字節(jié):時(shí)間戳
3.3 屏幕捕獲與編碼
主控端調(diào)用
MediaProjectionManager.createScreenCaptureIntent()
,請(qǐng)求授權(quán)。授權(quán)通過(guò)后,獲取
MediaProjection
,創(chuàng)建VirtualDisplay
并綁定ImageReader.getSurface()
。在獨(dú)立線程中,通過(guò)
ImageReader.acquireLatestImage()
不斷獲取原始Image
。將
Image
轉(zhuǎn)為Bitmap
,然后使用Bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream)
編碼。將 JPEG 字節(jié)根據(jù)協(xié)議拼接幀頭,發(fā)送至受控端。
3.4 網(wǎng)絡(luò)傳輸與解碼
主控端
使用單例
SocketClient
管理連接。將編碼后的幀數(shù)據(jù)寫(xiě)入
BufferedOutputStream
,并在必要時(shí)調(diào)用flush()
。
受控端
啟動(dòng)
ScreenReceiverService
,監(jiān)聽(tīng)端口,接受連接。使用
BufferedInputStream
,先讀取 12 字節(jié)幀頭,再根據(jù)長(zhǎng)度讀完數(shù)據(jù)。將 JPEG 數(shù)據(jù)用
BitmapFactory.decodeByteArray()
解碼,更新到SurfaceView
。
3.5 輸入事件捕獲與模擬
主控端
在
MainActivity
上監(jiān)聽(tīng)觸摸事件onTouchEvent(MotionEvent)
,提取事件類(lèi)型與坐標(biāo)。按協(xié)議封裝成事件幀,發(fā)送至受控端。
受控端
RemoteControlService
接收事件幀后,通過(guò)無(wú)障礙接口構(gòu)造GestureDescription
:
Path path = new Path(); path.moveTo(x, y); GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(path, 0, 1);
調(diào)用
dispatchGesture(stroke, callback, handler)
注入觸摸。
四、完整代碼
/************************** MainActivity.java **************************/ package com.example.screencast; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; import android.media.Image; import android.media.ImageReader; import android.media.projection.MediaProjection; import android.media.projection.MediaProjectionManager; import android.os.Bundle; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; import android.widget.Button; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.net.Socket; /* * MainActivity:負(fù)責(zé) * 1. 請(qǐng)求屏幕捕獲權(quán)限 * 2. 啟動(dòng) ScreenShareService * 3. 捕獲觸摸事件并發(fā)送 */ public class MainActivity extends Activity { private static final int REQUEST_CODE_CAPTURE = 100; private MediaProjectionManager mProjectionManager; private MediaProjection mMediaProjection; private ImageReader mImageReader; private VirtualDisplay mVirtualDisplay; private ScreenShareService mShareService; private Button mStartBtn, mStopBtn; private Socket mSocket; private BufferedOutputStream mOut; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mStartBtn = findViewById(R.id.btn_start); mStopBtn = findViewById(R.id.btn_stop); // 點(diǎn)擊開(kāi)始:請(qǐng)求授權(quán)并啟動(dòng)服務(wù) mStartBtn.setOnClickListener(v -> startCapture()); // 點(diǎn)擊停止:停止服務(wù)并斷開(kāi)連接 mStopBtn.setOnClickListener(v -> { mShareService.stop(); }); } /** 請(qǐng)求屏幕捕獲授權(quán) */ private void startCapture() { mProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_CAPTURE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_CAPTURE && resultCode == RESULT_OK) { mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data); // 初始化 ImageReader 和 VirtualDisplay setupVirtualDisplay(); // 啟動(dòng)服務(wù) mShareService = new ScreenShareService(mMediaProjection, mImageReader); mShareService.start(); } } /** 初始化虛擬顯示器用于屏幕捕獲 */ private void setupVirtualDisplay() { DisplayMetrics metrics = getResources().getDisplayMetrics(); mImageReader = ImageReader.newInstance(metrics.widthPixels, metrics.heightPixels, PixelFormat.RGBA_8888, 2); mVirtualDisplay = mMediaProjection.createVirtualDisplay("ScreenCast", metrics.widthPixels, metrics.heightPixels, metrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null); } /** 捕獲觸摸事件并發(fā)送至受控端 */ @Override public boolean onTouchEvent(MotionEvent event) { if (mShareService != null && mShareService.isRunning()) { mShareService.sendTouchEvent(event); } return super.onTouchEvent(event); } } /************************** ScreenShareService.java **************************/ package com.example.screencast; import android.graphics.Bitmap; import android.graphics.ImageFormat; import android.media.Image; import android.media.ImageReader; import android.media.projection.MediaProjection; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.net.Socket; /* * ScreenShareService:負(fù)責(zé) * 1. 建立 Socket 連接 * 2. 從 ImageReader 獲取屏幕幀 * 3. 編碼后發(fā)送 * 4. 接收觸摸事件發(fā)送 */ public class ScreenShareService { private MediaProjection mProjection; private ImageReader mImageReader; private Socket mSocket; private BufferedOutputStream mOut; private volatile boolean mRunning; private HandlerThread mEncodeThread; private Handler mEncodeHandler; public ScreenShareService(MediaProjection projection, ImageReader reader) { mProjection = projection; mImageReader = reader; // 創(chuàng)建后臺(tái)線程處理編碼與網(wǎng)絡(luò) mEncodeThread = new HandlerThread("EncodeThread"); mEncodeThread.start(); mEncodeHandler = new Handler(mEncodeThread.getLooper()); } /** 啟動(dòng)服務(wù):連接服務(wù)器并開(kāi)始捕獲發(fā)送 */ public void start() { mRunning = true; mEncodeHandler.post(this::connectAndShare); } /** 停止服務(wù) */ public void stop() { mRunning = false; try { if (mSocket != null) mSocket.close(); mEncodeThread.quitSafely(); } catch (Exception ignored) {} } /** 建立 Socket 連接并循環(huán)捕獲發(fā)送 */ private void connectAndShare() { try { mSocket = new Socket("192.168.1.100", 8888); mOut = new BufferedOutputStream(mSocket.getOutputStream()); while (mRunning) { Image image = mImageReader.acquireLatestImage(); if (image != null) { sendImageFrame(image); image.close(); } } } catch (Exception e) { Log.e("ScreenShare", "連接或發(fā)送失敗", e); } } /** 發(fā)送圖像幀 */ private void sendImageFrame(Image image) throws Exception { // 將 Image 轉(zhuǎn) Bitmap、壓縮為 JPEG Image.Plane plane = image.getPlanes()[0]; ByteBuffer buffer = plane.getBuffer(); int width = image.getWidth(), height = image.getHeight(); Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bmp.copyPixelsFromBuffer(buffer); ByteArrayOutputStream baos = new ByteArrayOutputStream(); bmp.compress(Bitmap.CompressFormat.JPEG, 40, baos); byte[] jpegData = baos.toByteArray(); // 寫(xiě)幀頭:類(lèi)型=1, 長(zhǎng)度, 時(shí)間戳 mOut.write(intToBytes(1)); mOut.write(intToBytes(jpegData.length)); mOut.write(longToBytes(System.currentTimeMillis())); // 寫(xiě)圖像數(shù)據(jù) mOut.write(jpegData); mOut.flush(); } /** 發(fā)送觸摸事件 */ public void sendTouchEvent(MotionEvent ev) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((byte) ev.getAction()); baos.write(floatToBytes(ev.getX())); baos.write(floatToBytes(ev.getY())); baos.write(longToBytes(ev.getEventTime())); byte[] data = baos.toByteArray(); mOut.write(intToBytes(2)); mOut.write(intToBytes(data.length)); mOut.write(longToBytes(System.currentTimeMillis())); mOut.write(data); mOut.flush(); } catch (Exception ignored) {} } // …(byte/int/long/float 與 bytes 相互轉(zhuǎn)換方法,略) } /************************** RemoteControlService.java **************************/ package com.example.screencast; import android.accessibilityservice.AccessibilityService; import android.graphics.Path; import android.view.accessibility.GestureDescription; import java.io.BufferedInputStream; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /* * RemoteControlService(繼承 AccessibilityService) * 1. 啟動(dòng) ServerSocket,接收主控端連接 * 2. 循環(huán)讀取幀頭與數(shù)據(jù) * 3. 區(qū)分圖像幀與事件幀并處理 */ public class RemoteControlService extends AccessibilityService { private ServerSocket mServerSocket; private Socket mClient; private BufferedInputStream mIn; private volatile boolean mRunning; @Override public void onServiceConnected() { super.onServiceConnected(); new Thread(this::startServer).start(); } /** 啟動(dòng)服務(wù)端 socket */ private void startServer() { try { mServerSocket = new ServerSocket(8888); mClient = mServerSocket.accept(); mIn = new BufferedInputStream(mClient.getInputStream()); mRunning = true; while (mRunning) { handleFrame(); } } catch (Exception e) { e.printStackTrace(); } } /** 處理每個(gè)數(shù)據(jù)幀 */ private void handleFrame() throws Exception { byte[] header = new byte[12]; mIn.read(header); int type = bytesToInt(header, 0); int len = bytesToInt(header, 4); // long ts = bytesToLong(header, 8); byte[] payload = new byte[len]; int read = 0; while (read < len) { read += mIn.read(payload, read, len - read); } if (type == 1) { // 圖像幀:解碼并渲染到 SurfaceView handleImageFrame(payload); } else if (type == 2) { // 觸摸事件:模擬 handleTouchEvent(payload); } } /** 解碼 JPEG 并更新 UI(通過(guò) Broadcast 或 Handler 通信) */ private void handleImageFrame(byte[] data) { // …(略,解碼 Bitmap 并 post 到 SurfaceView) } /** 根據(jù)協(xié)議解析并 dispatchGesture */ private void handleTouchEvent(byte[] data) { int action = data[0]; float x = bytesToFloat(data, 1); float y = bytesToFloat(data, 5); // long t = bytesToLong(data, 9); Path path = new Path(); path.moveTo(x, y); GestureDescription.StrokeDescription sd = new GestureDescription.StrokeDescription(path, 0, 1); dispatchGesture(new GestureDescription.Builder().addStroke(sd).build(), null, null); } @Override public void onInterrupt() {} }
<!-- AndroidManifest.xml --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.screencast"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:label="ScreenCast"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <service android:name=".RemoteControlService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService"/> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config"/> </service> </application> </manifest>
<!-- activity_main.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="開(kāi)始屏幕共享"/> <Button android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="停止服務(wù)"/> <SurfaceView android:id="@+id/surface_view" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
五、代碼解讀
MainActivity
請(qǐng)求并處理用戶(hù)授權(quán),創(chuàng)建并綁定
VirtualDisplay
;啟動(dòng)
ScreenShareService
負(fù)責(zé)捕獲與發(fā)送;重寫(xiě)
onTouchEvent
,將觸摸事件傳給服務(wù)。
ScreenShareService
在后臺(tái)線程中建立 TCP 連接;
循環(huán)從
ImageReader
獲取幀,將其轉(zhuǎn)為Bitmap
并壓縮后通過(guò) Socket 發(fā)送;監(jiān)聽(tīng)主控端觸摸事件,封裝并發(fā)送事件幀。
RemoteControlService
作為無(wú)障礙服務(wù)啟動(dòng),監(jiān)聽(tīng)端口接收數(shù)據(jù);
讀取幀頭與載荷,根據(jù)類(lèi)型分發(fā)到圖像處理或觸摸處理;
觸摸處理時(shí)使用
dispatchGesture
注入軌跡,實(shí)現(xiàn)遠(yuǎn)程控制。
布局與權(quán)限
在
AndroidManifest.xml
中聲明必要權(quán)限與無(wú)障礙服務(wù);activity_main.xml
簡(jiǎn)單布局包含按鈕與SurfaceView
用于渲染。
六、項(xiàng)目總結(jié)
通過(guò)本項(xiàng)目,我們完整地實(shí)現(xiàn)了 Android 平臺(tái)上兩臺(tái)設(shè)備的屏幕共享與遠(yuǎn)程控制功能,掌握并綜合運(yùn)用了以下關(guān)鍵技術(shù):
MediaProjection API:原生屏幕捕獲與虛擬顯示創(chuàng)建;
Socket 編程:設(shè)計(jì)幀協(xié)議,實(shí)現(xiàn)高效、可靠的圖像與事件雙向傳輸;
圖像編碼/解碼:將屏幕幀壓縮為 JPEG,平衡清晰度與帶寬;
無(wú)障礙服務(wù):通過(guò)
dispatchGesture
注入觸摸事件,完成遠(yuǎn)程控制;多線程處理:使用
HandlerThread
保證捕獲、編碼、傳輸?shù)葘?shí)時(shí)性,避免 UI 阻塞。
這套方案具備以下擴(kuò)展方向:
音頻同步:在屏幕共享同時(shí)傳輸麥克風(fēng)或系統(tǒng)音頻。
視頻編解碼優(yōu)化:引入硬件 H.264 編碼,以更低延遲和更高壓縮率。
跨平臺(tái)支持:在 iOS、Windows 等平臺(tái)實(shí)現(xiàn)對(duì)應(yīng)客戶(hù)端。
安全性增強(qiáng):加入 TLS/SSL 加密,防止中間人攻擊;驗(yàn)證設(shè)備身份。
以上就是Android實(shí)現(xiàn)兩臺(tái)手機(jī)屏幕共享和遠(yuǎn)程控制功能的詳細(xì)內(nèi)容,更多關(guān)于Android手機(jī)屏幕共享和遠(yuǎn)程控制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android抽屜導(dǎo)航Navigation Drawer實(shí)例解析
這篇文章主要為大家詳細(xì)介紹了Android抽屜導(dǎo)航NavigationDrawer實(shí)例,感興趣的小伙伴們可以參考一下2016-05-05C/C++在Java、Android和Objective-C三大平臺(tái)下實(shí)現(xiàn)混合編程
本文主要介紹C/C++在Java、Android和Objective-C三大平臺(tái)下實(shí)現(xiàn)混合編程,這里舉例說(shuō)明實(shí)現(xiàn)不同平臺(tái)用C/C++實(shí)現(xiàn)編程的方法,有興趣的小伙伴可以參考下2016-08-08Android CameraX打開(kāi)攝像頭預(yù)覽教程
大家好,本篇文章主要講的是Android CameraX打開(kāi)攝像頭預(yù)覽教程,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2021-12-12Android編程實(shí)現(xiàn)自定義title功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)自定義title功能,結(jié)合具體實(shí)例形式分析了Android自定義title的具體實(shí)現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-03-03自定義View系列之kotlin繪制手勢(shì)設(shè)置溫度控件的方法
這篇文章主要給大家介紹了關(guān)于自定義View系列之kotlin繪制手勢(shì)設(shè)置溫度控件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07Android 廣播監(jiān)聽(tīng)網(wǎng)絡(luò)狀態(tài)詳解及實(shí)例代碼
這篇文章主要介紹了Android 廣播監(jiān)聽(tīng)網(wǎng)絡(luò)狀態(tài)詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02Android自定義View實(shí)現(xiàn)APP啟動(dòng)頁(yè)倒計(jì)時(shí)效果
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)APP啟動(dòng)頁(yè)倒計(jì)時(shí)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02Android利用ObjectAnimator實(shí)現(xiàn)ArcMenu
這篇文章主要為大家詳細(xì)介紹了Android利用ObjectAnimator實(shí)現(xiàn)ArcMenu的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-07-07Android獲取WebView加載url的請(qǐng)求錯(cuò)誤碼 【推薦】
這篇文章主要介紹了Android獲取WebView加載url的請(qǐng)求錯(cuò)誤碼 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06