Android实现两台手机屏幕共享和远程控制功能示例代码详解

Katie。 2025-04-23 10:30:24编程技术
595

在移动互联网飞速发展的今天,屏幕共享和远程控制功能已成为许多应用程序不可或缺的一部分。无论是远程协作、在线教育,还是技术支持和娱乐分享,这些功能都极大地丰富了我们的数字生活体验。Android平台作为最受欢迎的移动操作系统之一,自然也提供了丰富的API和工具来实现这些功能。本文将详细介绍如何在Android设备上实现两台手机之间的屏幕共享和远程控制功能,通过示例代码和详细步骤,帮助开发者快速掌握这一技能。

Android.webp

一、项目概述

在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 Android 手机之间的屏幕共享与远程控制,其核心功能包括:

  • 主控端(Controller):捕获自身屏幕并将实时画面编码后通过网络发送;同时监听用户在主控端的触摸、滑动和按键等输入操作,并将操作事件发送至受控端。

  • 受控端(Receiver):接收屏幕画面数据并实时解码、渲染到本地界面;接收并解析主控端的输入操作事件,通过系统接口模拟触摸和按键,实现被控设备的操作。

通过这一方案,用户可以实时“看到”受控端的屏幕,并在主控端进行点触、滑动等交互,达到“远程操控”他机的效果。本项目的核心难点在于如何保证图像数据的实时性与清晰度,以及如何准确、及时地模拟输入事件。

二、相关知识

2.1 MediaProjection API

  • 概述:Android 5.0(API 21)引入的屏幕录制和投影接口。通过 MediaProjectionManager 获取用户授权后,可创建 VirtualDisplay,将屏幕内容输送至 Surface 或 ImageReader

  • 关键类

    • MediaProjectionManager:请求屏幕捕获权限

    • MediaProjection:执行屏幕捕获

    • VirtualDisplay:虚拟显示、输出到 Surface

    • ImageReader:以 Image 帧的方式获取屏幕图像

2.2 Socket 网络通信

  • 概述:基于 TCP 协议的双向流式通信,适合大块数据的稳定传输。

  • 关键类

    • ServerSocket / Socket:服务端监听与客户端连接

    • InputStream / OutputStream:数据读写

  • 注意:需要设计简单高效的协议,在发送每帧图像前加上帧头(如长度信息),以便接收端正确分包、组帧。

2.3 输入事件模拟

  • 概述:在非系统应用中无法直接使用 InputManager 注入事件,需要借助无障碍服务(AccessibilityService)或系统签名权限。

  • 关键技术

    • 无障碍服务(AccessibilityService)注入触摸事件

    • 使用 GestureDescription 构造手势并通过 dispatchGesture 触发

2.4 数据压缩与传输优化

  • 图像编码:将 Image 帧转为 JPEG 或 H.264,以减小带宽占用。

  • 数据分片:对大帧进行分片发送,防止单次写入阻塞或触发 OutOfMemoryError

  • 网络缓冲与重传:TCP 本身提供重传,但需控制合适的发送速率,防止拥塞。

2.5 多线程与异步处理

  • 概述:屏幕捕获与网络传输耗时,需放在独立线程或 HandlerThread 中,否则 UI 会卡顿。

  • 框架

    • ThreadPoolExecutor 管理捕获、编码、发送任务

    • HandlerThread 配合 Handler 处理 IO 回调

三、实现思路

3.1 架构设计

+--------------+                                +--------------+
|              |--(请求授权)------------------->|              |
| MainActivity |                                | RemoteActivity|
|              |<-(启动服务、连接成功)-----------|              |
+------+-------+                                +------+-------+
       |                                                |
       | 捕获屏幕 -> MediaProjection -> ImageReader      | 接收画面 -> 解码 -> SurfaceView
       | 编码(JPEG/H.264)                               | 
       | 发送 -> Socket OutputStream                     | 
       |                                                | 接收事件 -> 无障碍 Service -> dispatchGesture
       |<--触摸事件包------------------------------------|
       | 模拟触摸 => AccessibilityService                |
+------+-------+                                +------+-------+
| ScreenShare  |                                | RemoteControl|
|   Service    |                                |   Service    |
+--------------+                                +--------------+

3.2 协议与数据格式

  • 帧头结构(12 字节)

    • 4 字节:帧类型(0x01 表示图像,0x02 表示触摸事件)

    • 4 字节:数据长度 N(网络字节序)

    • 4 字节:时间戳(毫秒)

  • 图像帧数据[帧头][JPEG 数据]

  • 触摸事件数据

    • 1 字节:事件类型(0:DOWN,1:MOVE,2:UP)

    • 4 字节:X 坐标(float)

    • 4 字节:Y 坐标(float)

    • 8 字节:时间戳

3.3 屏幕捕获与编码

  1. 主控端调用 MediaProjectionManager.createScreenCaptureIntent(),请求授权。

  2. 授权通过后,获取 MediaProjection,创建 VirtualDisplay 并绑定 ImageReader.getSurface()

  3. 在独立线程中,通过 ImageReader.acquireLatestImage() 不断获取原始 Image

  4. 将 Image 转为 Bitmap,然后使用 Bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream) 编码。

  5. 将 JPEG 字节根据协议拼接帧头,发送至受控端。

3.4 网络传输与解码

主控端

  • 使用单例 SocketClient 管理连接。

  • 将编码后的帧数据写入 BufferedOutputStream,并在必要时调用 flush()

受控端

  • 启动 ScreenReceiverService,监听端口,接受连接。

  • 使用 BufferedInputStream,先读取 12 字节帧头,再根据长度读完数据。

  • 将 JPEG 数据用 BitmapFactory.decodeByteArray() 解码,更新到 SurfaceView

3.5 输入事件捕获与模拟

主控端

  • 在 MainActivity 上监听触摸事件 onTouchEvent(MotionEvent),提取事件类型与坐标。

  • 按协议封装成事件帧,发送至受控端。

受控端

  • RemoteControlService 接收事件帧后,通过无障碍接口构造 GestureDescription

Path path = new Path();
path.moveTo(x, y);
GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(path, 0, 1);
  • 调用 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:负责
 * 1. 请求屏幕捕获权限
 * 2. 启动 ScreenShareService
 * 3. 捕获触摸事件并发送
 */
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);
 
        // 点击开始:请求授权并启动服务
        mStartBtn.setOnClickListener(v -> startCapture());
        // 点击停止:停止服务并断开连接
        mStopBtn.setOnClickListener(v -> {
            mShareService.stop();
        });
    }
 
    /** 请求屏幕捕获授权 */
    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();
            // 启动服务
            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);
    }
 
    /** 捕获触摸事件并发送至受控端 */
    @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:负责
 * 1. 建立 Socket 连接
 * 2. 从 ImageReader 获取屏幕帧
 * 3. 编码后发送
 * 4. 接收触摸事件发送
 */
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;
        // 创建后台线程处理编码与网络
        mEncodeThread = new HandlerThread("EncodeThread");
        mEncodeThread.start();
        mEncodeHandler = new Handler(mEncodeThread.getLooper());
    }
 
    /** 启动服务:连接服务器并开始捕获发送 */
    public void start() {
        mRunning = true;
        mEncodeHandler.post(this::connectAndShare);
    }
 
    /** 停止服务 */
    public void stop() {
        mRunning = false;
        try {
            if (mSocket != null) mSocket.close();
            mEncodeThread.quitSafely();
        } catch (Exception ignored) {}
    }
 
    /** 建立 Socket 连接并循环捕获发送 */
    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", "连接或发送失败", e);
        }
    }
 
    /** 发送图像帧 */
    private void sendImageFrame(Image image) throws Exception {
        // 将 Image 转 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();
 
        // 写帧头:类型=1, 长度, 时间戳
        mOut.write(intToBytes(1));
        mOut.write(intToBytes(jpegData.length));
        mOut.write(longToBytes(System.currentTimeMillis()));
        // 写图像数据
        mOut.write(jpegData);
        mOut.flush();
    }
 
    /** 发送触摸事件 */
    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 相互转换方法,略)
}
 
/************************** 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. 启动 ServerSocket,接收主控端连接
 * 2. 循环读取帧头与数据
 * 3. 区分图像帧与事件帧并处理
 */
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();
    }
 
    /** 启动服务端 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();
        }
    }
 
    /** 处理每个数据帧 */
    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(通过 Broadcast 或 Handler 通信) */
    private void handleImageFrame(byte[] data) {
        // …(略,解码 Bitmap 并 post 到 SurfaceView)
    }
 
    /** 根据协议解析并 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="开始屏幕共享"/>
    <Button
        android:id="@+id/btn_stop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="停止服务"/>
    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

五、代码解读

  1. MainActivity

    • 请求并处理用户授权,创建并绑定 VirtualDisplay

    • 启动 ScreenShareService 负责捕获与发送;

    • 重写 onTouchEvent,将触摸事件传给服务。

  2. ScreenShareService

    • 在后台线程中建立 TCP 连接;

    • 循环从 ImageReader 获取帧,将其转为 Bitmap 并压缩后通过 Socket 发送;

    • 监听主控端触摸事件,封装并发送事件帧。

  3. RemoteControlService

    • 作为无障碍服务启动,监听端口接收数据;

    • 读取帧头与载荷,根据类型分发到图像处理或触摸处理;

    • 触摸处理时使用 dispatchGesture 注入轨迹,实现远程控制。

  4. 布局与权限

    • 在 AndroidManifest.xml 中声明必要权限与无障碍服务;

    • activity_main.xml 简单布局包含按钮与 SurfaceView 用于渲染。

六、项目总结

通过本项目,我们完整地实现了 Android 平台上两台设备的屏幕共享与远程控制功能,掌握并综合运用了以下关键技术:

  • MediaProjection API:原生屏幕捕获与虚拟显示创建;

  • Socket 编程:设计帧协议,实现高效、可靠的图像与事件双向传输;

  • 图像编码/解码:将屏幕帧压缩为 JPEG,平衡清晰度与带宽;

  • 无障碍服务:通过 dispatchGesture 注入触摸事件,完成远程控制;

  • 多线程处理:使用 HandlerThread 保证捕获、编码、传输等实时性,避免 UI 阻塞。

这套方案具备以下扩展方向:

  1. 音频同步:在屏幕共享同时传输麦克风或系统音频。

  2. 视频编解码优化:引入硬件 H.264 编码,以更低延迟和更高压缩率。

  3. 跨平台支持:在 iOS、Windows 等平台实现对应客户端。

  4. 安全性增强:加入 TLS/SSL 加密,防止中间人攻击;验证设备身份。

通过本文的介绍,我们了解了在Android平台上实现两台手机屏幕共享和远程控制功能的基本原理和实现步骤。利用MediaProjection API进行屏幕捕获,通过Socket编程建立网络连接,实现屏幕图像和触摸事件的实时传输,这些都是实现该功能的关键技术。同时,我们也看到了在实际开发中需要考虑的权限请求、图像处理和用户体验等方面的细节。希望本文的示例代码和详细讲解能够帮助开发者更好地理解并实现这一功能,为用户带来更加便捷和高效的远程协作和分享体验。随着技术的不断进步,屏幕共享和远程控制功能将在更多领域发挥重要作用,为我们的生活和工作带来更多便利。

Android 手机屏幕共享 手机远程控制手机
THE END
蜜芽
故事不长,也不难讲,四字概括,毫无意义。

相关推荐

Android Studio 中使用 SQLite 数据库开发完整指南(Kotlin版本)
对于Kotlin开发者来说,掌握在Android Studio中使用SQLite数据库进行数据存储和管理的方法是必不可少的技能。本文将提供一份完整的指南,帮助Kotlin开发者快速上手并在Androi...
2025-04-30 编程技术
411

Android Studio下载及安装图文教程(详细版)
随着移动应用的快速发展,Android Studio作为官方推荐的集成开发环境(IDE),已经成为Android开发者不可或缺的工具。为了帮助新手快速上手并高效地进行Android开发,本文将提供...
2025-04-28 编程技术
746

使用Docker和cpolar在Ubuntu上部署可远程访问的Android模拟器
本文将介绍一种在Linux Ubuntu系统上使用Docker部署Android模拟器,并通过cpolar内网穿透工具实现远程访问的全新方案。通过这一方案,开发者可以轻松地在任何地方通过公网地址...
2025-03-28 编程技术
402

抖音离线缓存视频怎么保存到相册?(Android+IOS)
许多用户喜欢在闲暇时浏览抖音,并希望将喜爱的视频保存到本地相册以便随时观看。本文ZHANID工具网将详细介绍如何将抖音离线缓存的视频保存到手机相册,涵盖iOS和Android两大...
2025-03-26 自媒体
1969

Android Studio集成Gemini新增多模态功能,开发者可上传图像获取UI代码
谷歌最新宣布,Android Studio中的Gemini助手已升级支持多模态输入功能,开发者现在可以直接将图像附加到提示中,以获取应用程序开发过程中的视觉辅助。
2025-03-14 新闻资讯
363

基于Android实现计算器计算功能
这篇文章主要介绍了基于Android实现计算器计算功能,文章通过代码示例给大家讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
2024-06-16 编程技术
371