远程控制平台三之实现模拟点击

手势捕捉

手势的捕捉是比较简单的,只需设置一个主控端SurfaceView的触摸监听即可,需要注意的是,短按、长按和滑动要区分开,我这里是z在OnTouchListener里简单区分,而且没有进行曲线动作的捕获,有需要的可以在这个监听里去完善。

public class ActionListener implements View.OnTouchListener {
    private int mTouchStartX;
    private int mTouchStartY;
    private int mTouchCurrentX;
    private int mTouchCurrentY;
    private int startX;
    private int startY;
    private int endX;
    private int endY;
    private float screenWidth = ScreenUtils.getScreenWidth();
    private float screenHeight = ScreenUtils.getScreenHeight();

    private long startMillis;
    private boolean longPressPerformed;

    public ActionListener(View screenView) {
        screenView.post(new Runnable() {
            @Override
            public void run() {
                screenWidth = screenView.getWidth() * 1.0f;
                screenHeight = screenView.getHeight() * 1.0f;
            }
        });
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        L.e("触摸位置float: " + event.getX() + ", " + event.getY() + ", action" + event.getAction());
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            longPressPerformed = false;
            startMillis = System.currentTimeMillis();

            mTouchStartX = (int) event.getX();
            mTouchStartY = (int) event.getY();

            startX = mTouchStartX;
            startY = mTouchStartY;
            endX = startX;
            endY = startY;
            return true;
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            mTouchCurrentX = (int) event.getX();
            mTouchCurrentY = (int) event.getY();
            mTouchStartX = mTouchCurrentX;
            mTouchStartY = mTouchCurrentY;
            endX = mTouchCurrentX;
            endY = mTouchCurrentY;
            boolean longThanLongPress = System.currentTimeMillis()-startMillis > 380;
            if (longThanLongPress && !longPressPerformed) {
                int horizonAbs = Math.abs(endX - startX);
                int verticalAbs = Math.abs(endY - startY);
                if (horizonAbs < 100 && verticalAbs < 100) {
                    longPressPerformed = true;
                    MessageSender.INSTANCE.sendCmd(ActionType.LONG_CLICK, startX/screenWidth, startY/screenHeight, endX/screenWidth, endY/screenHeight, System.currentTimeMillis()-startMillis);
                }
            }
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            // 已经触发了长按
            if (longPressPerformed) {
                return false;
            }
            if (mTouchCurrentX == 0) {
                L.e("点击");
                MessageSender.INSTANCE.sendCmd(ActionType.CLICK, startX/screenWidth, startY/screenHeight, endX/screenWidth, endY/screenHeight, System.currentTimeMillis()-startMillis);
            } else {
                int horizonAbs = Math.abs(endX - startX);
                int verticalAbs = Math.abs(endY - startY);
                if (horizonAbs > 100 || verticalAbs > 100) {
                    L.e("滑动");
                    MessageSender.INSTANCE.sendCmd(ActionType.SWIPE, startX/screenWidth, startY/screenHeight, endX/screenWidth, endY/screenHeight, System.currentTimeMillis()-startMillis);
                } else {
                    L.e("滑动距离太小,当做点击(" + startX + "," + startY + ") -> (" + endX + ", " + endY + ")");
                    MessageSender.INSTANCE.sendCmd(ActionType.CLICK, startX/screenWidth, startY/screenHeight, endX/screenWidth, endY/screenHeight, System.currentTimeMillis()-startMillis);
                }
            }
        }
        // 为了防止出现只有ACTION_DOWN和ACTION_MOVE,没有ACTION_UP的问题
        return false;
    }
}

手势传递

手势的传递其实和推拉流是一样的,我这里也是使用了JeroMQ的“订阅-发布”模式,但和推拉流方向相反,这里的手势消息是从主控端到受控端。

object MessageSender {
    private val controlExecutorService = Executors.newSingleThreadExecutor()

    @Volatile
    private var controlSocket: ZMQ.Socket? = null

    @Volatile
    private var controlContext: ZContext? = null

    @Volatile
    private var controlCmdUrl = ""

    /**
     * 主控端连接服务器
     */
    fun startControlService() {
        controlCmdUrl = "tcp://${FollowManager.currentFollowBean.p}:${KeyValue.receivePortCmd}"
        controlExecutorService.submit(Runnable {
            if (controlSocket != null) {
                return@Runnable
            }

            try {
                controlContext = ZContext()
                controlSocket = controlContext?.createSocket(SocketType.PUB)
                controlSocket?.enableCommonAuth()
                controlSocket?.enableCommonChat()
                controlSocket?.connect(controlCmdUrl)

                // 点击受控端列表item时,会先发送两个指令过去(让受控端开始推流),先等待几秒再发送,因为建立连接需要时间,太早发送会失败
                Thread.sleep(3500)
                sendCmd(ActionType.CLICK, 0.0f, 0.0f, 0.0f, 1.0f, ActionManager.DURATION_CLICK_START)
            } catch (e: Exception) {
                AppUtils.relaunchAppOnChildThread("主控端命令服务失败")
            }
        })
    }

    fun startControl() {
        GlobalContext.getContext().run {
            val controllerIntent = Intent(this, ControlService::class.java)
            ContextCompat.startForegroundService(this, controllerIntent)
        }
    }

    /**
     * 发送指令给受控端,每一个参数都不能为空
     * 命令格式:收信人ID~动作类型(短击,长按,滑动)~startX~startY~endX~endY~发送时间戳(这里的浮点数是屏幕比例不是像素)
     */
    fun sendCmd(
        touchType: Int,
        startX: Float,
        startY: Float,
        endX: Float,
        endY: Float,
        duration: Long
    ) {
        val deviceId = FollowManager.currentFollowBean.i
        if (TextUtils.isEmpty(deviceId)) {
            return
        }
        controlExecutorService.submit(Runnable {
            try {
                controlSocket?.send("${deviceId}${MESSAGE_SPLIT}${touchType}${MESSAGE_SPLIT}${startX}${MESSAGE_SPLIT}${startY}${MESSAGE_SPLIT}${endX}${MESSAGE_SPLIT}${endY}${MESSAGE_SPLIT}${duration}")
            } catch (e: Exception) {
                L.e(e.message)
            }
        })
    }

    fun sendCmdToExplicitDevice(
        touchType: Int,
        startX: Float,
        startY: Float,
        endX: Float,
        endY: Float,
        duration: Long,
        deviceId: String
    ) {
        if (TextUtils.isEmpty(deviceId)) {
            return
        }
        controlExecutorService.submit(Runnable {
            try {
                controlSocket?.send("${deviceId}${MESSAGE_SPLIT}${touchType}${MESSAGE_SPLIT}${startX}${MESSAGE_SPLIT}${startY}${MESSAGE_SPLIT}${endX}${MESSAGE_SPLIT}${endY}${MESSAGE_SPLIT}${duration}")
            } catch (e: Exception) {
                L.e(e.message)
            }
        })
    }



    fun stopControl() {
        GlobalContext.getContext().run {
            val controllerIntent = Intent(this, ControlService::class.java)
            stopService(controllerIntent)
        }
    }

    fun recycleControlService() {
        try {
            controlSocket?.disconnect(controlCmdUrl)
        } catch (e: Exception) {
            L.e(e.message)
        }

        try {
            controlContext?.destroy()
        } catch (e: Exception) {
            L.e(e.message)
        }

        controlContext = null
        controlSocket = null
    }
}

模拟点击

如何实现一个类似手机版按键精灵的功能(模拟点击)?我们需要用到无障碍功能,即实现一个继承自AccessibilityService的类,注意相关配置。

class ActionService : AccessibilityService() {
    override fun onCreate() {
        super.onCreate()
        startForegroundNotification()
        ActionManager.service = this
    }

    override fun onDestroy() {
        super.onDestroy()
        ActionManager.service = null
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
    }
    override fun onInterrupt() {}

    override fun onKeyEvent(event: KeyEvent?): Boolean {
        return super.onKeyEvent(event)
    }
}

清单文件里声明:

<service
    android:name=".ui.follow.action.ActionService"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
    android:foregroundServiceType="mediaProjection|dataSync">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibility_service_config" />
</service>

accessibility_service_config.xml

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagRequestFilterKeyEvents"
    android:canPerformGestures="true"
    android:canRequestFilterKeyEvents="true"
    android:canRequestTouchExplorationMode="true"
    android:canRetrieveWindowContent="true"
    android:description="@string/action_desc"
    android:notificationTimeout="100" />

当受控端收到主控端发过来的命令后,解析并执行:

private fun parseCmd(cmdArray: List<String>) {
    // 没有无障碍权限
    if (!ScreenManager.accessibilityEnabled) {
        return
    }

    // 收信人ID~动作类型(短击,长按,滑动)~startX~startY~endX~endY~动作时长(这里的浮点数是屏幕比例不是像素)
    val touchType = cmdArray[1].toInt()
    val startX = cmdArray[2].toFloat()
    val startY = cmdArray[3].toFloat()
    val endX = cmdArray[4].toFloat()
    val endY = cmdArray[5].toFloat()
    val duration = cmdArray[6].toLong()
    lastCmdMillis = if (touchType == ActionType.CONTROL_LEAVE) {
        0L
    } else {
        ActionManager.wakeupScreenIfNeed()
        System.currentTimeMillis()
    }
    when (touchType) {
        ActionType.CLICK -> {
            ActionManager.click(endX, endY)
        }
        ActionType.LONG_CLICK -> {
            ActionManager.longClick(endX, endY)
        }
        ActionType.SWIPE -> {
            ActionManager.swipe(startX, startY, endX, endY, duration)
        }
        ActionType.DESKTOP -> {
            ActionManager.goDesktop()
        }
        ActionType.BACK -> {
            ActionManager.goBack()
        }
        else -> {
            L.e("未知动作")
        }
    }
}
object ActionManager {
    const val DURATION_CLICK_START = 100L
    private const val DURATION_CLICK = 300L
    private const val DURATION_LONG_CLICK = 2000L
    private val screenWidth = ScreenUtils.getScreenWidth()
    private val screenHeight = ScreenUtils.getScreenHeight()

    @Volatile
    var service : ActionService? = null

    private val xMiddle = screenWidth / 2.0f
    private val yMiddle = screenHeight / 2.0f

    private val xLeft = xMiddle / 2
    private val xRight = xLeft * 3

    private val yTop = yMiddle / 2
    private val yBottom = yTop * 3

    /**
     * 从左往右滑动
     */
    fun swipeLeft2Right() {
        val path = Path().apply {
            //moveTo(xLeft, yMiddle)
            moveTo(50.0f, yMiddle)
            lineTo(xRight, yMiddle)
        }
        dispatchSwipeGesture(path)
    }

    /**
     * 从右往左滑动
     */
    fun swipeRight2Left() {
        val path = Path().apply {
            moveTo(xRight, yMiddle)
            lineTo(xLeft, yMiddle)
        }
        dispatchSwipeGesture(path)
    }

    /**
     * 从上往下滑动
     */
    fun swipeTop2Bottom() {
        val path = Path().apply {
            moveTo(xMiddle, yTop)
            lineTo(xMiddle, yBottom)
        }
        dispatchSwipeGesture(path)
    }

    /**
     * 从下往上滑动
     */
    fun swipeBottom2Top() {
        val path = Path().apply {
            moveTo(xMiddle, yBottom)
            lineTo(xMiddle, yTop)
        }
        dispatchSwipeGesture(path)
    }

    /**
     * 滑动
     */
    fun swipe(startX: Float, startY: Float, endX: Float, endY: Float, duration: Long = DURATION_LONG_CLICK) {
        val realStartX = if (startX < 0) 0.001f else startX
        val realStartY = if (startY < 0) 0.001f else startY
        val realEndX = if (endX < 0) 0.001f else endX
        val realEndY = if (endY < 0) 0.001f else endY
        var startPixelX = realStartX * screenWidth
        var startPixelY = realStartY * screenHeight

        if (startPixelX < 100) {
            startPixelX = 1.0f
        } else if (startPixelX > screenWidth -100) {
            startPixelX = screenWidth * 1.0f - 5
        }

        if (startPixelY < 100) {
            startPixelY = 1.0f
        } else if (startPixelY > screenHeight -100) {
            startPixelY = screenHeight * 1.0f - 5
        }

        val path = Path().apply {
            moveTo(startPixelX, startPixelY)
            lineTo(realEndX * screenWidth, realEndY * screenWidth)
        }
        dispatchSwipeGesture(path, duration)
    }

    /**
     * 点击事件
     * 这里的坐标轴可以通过打开开发者选项里的“指针位置(屏幕叠加层显示当前触摸数据)”来获取,这种通过点击指定像素位置的
     * 实现无法做到兼容所有设备,如果要点击特定按钮,可以通过在service里查找控件并触发点击
     */
    fun click(x: Float, y: Float) {
        L.e("点击事件")

        var startPixelX = x * screenWidth
        if (startPixelX < 100) {
            startPixelX = 1.0f
        } else if (startPixelX > screenWidth -100) {
            startPixelX = screenWidth * 1.0f - 5
        }

        var startPixelY = y * screenHeight
        if (startPixelY < 100) {
            startPixelY = 1.0f
        } else if (startPixelY > screenHeight -100) {
            startPixelY = screenHeight * 1.0f - 5
        }

        GlobalContext.runOnUiThread {
            val clickPath = Path()
            clickPath.moveTo(startPixelX, startPixelY)
            val clickStroke = StrokeDescription(clickPath, DURATION_CLICK_START, DURATION_CLICK)
            val gestureDescription = GestureDescription.Builder().addStroke(clickStroke).build()
            service?.dispatchGesture(gestureDescription, null, null)
        }
    }

    fun longClick(x: Float, y: Float) {
        GlobalContext.runOnUiThread {
            val clickPath = Path()
            clickPath.moveTo(x * screenWidth, y * screenHeight)
            val clickStroke = StrokeDescription(clickPath, DURATION_CLICK_START, DURATION_LONG_CLICK)
            val gestureDescription = GestureDescription.Builder().addStroke(clickStroke).build()
            service?.dispatchGesture(gestureDescription, null, null)
        }
    }


    /**
     * 模拟手势滑动
     */
    private fun dispatchSwipeGesture(path: Path, duration: Long = DURATION_LONG_CLICK) {
        L.e("模拟手势滑动")
        GlobalContext.runOnUiThread {
            val stroke = GestureDescription.StrokeDescription(path, DURATION_CLICK_START, DURATION_CLICK)
            service?.dispatchGesture(
                GestureDescription.Builder().addStroke(stroke).build(), null, null
            )
        }
    }

    /**
     * 回到桌面
     */
    fun goDesktop() {
        GlobalContext.runOnUiThread {
            ActivityUtils.startHomeActivity()
        }
    }

    /**
     * 模拟返回
     */
    fun goBack() {
        GlobalContext.runOnUiThread {
            service?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
        }
    }

    /**
     * 如果屏幕没亮则唤起屏幕
     */
    fun wakeupScreenIfNeed() {
        if (!isScreenOn() || MessageReceiver.isControlLeave()) {
            L.e("唤起屏幕")
            AppUtils.wakeupScreen()
        } else {
            L.e("屏幕已亮")
        }
    }

    /**
     * 当前屏幕是否亮了
     */
    private fun isScreenOn(): Boolean {
        val dm = GlobalContext.getContext().getSystemService(AppCompatActivity.DISPLAY_SERVICE) as DisplayManager
        for (display in dm.displays) {
            if (display.state != Display.STATE_ON) {
                return false
            }
        }
        return true
    }

    /**
     * 巧妙唤醒屏幕(先弹出关机对话框,再取消这个对话框)
     */
    private fun wakeScreenSkillfully() {
        GlobalContext.runOnUiThread {
            service?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG)
            click(1.0f, 1.0f)
        }
    }
}

精准点击

以上虽然实现了模拟点击的操作,但留下了一个疑问,主控端点击屏幕某个按钮,如何让受控端也模拟点击相应的位置?因为我们都知道,Android设备碎片化太严重了,各种分辨率、尺寸的屏幕,除非主控端和受控端屏幕参数完全一致,否则位置大概率会有所偏差。刚开始我是想计算当前屏幕点击位置的像素位置,传递到受控端后,转换相应的像素,但效果不佳,后来想到一个简单粗暴的方法:干脆不用传像素了,就传屏幕位置的比例,比如在主控端点击的是A(x,y)位置,那么我就计算A位置的x和y分别占当前设备的比例,假设为x’和y’,把(x’, y’)传到受控端,分别乘以受控端的宽和高,就得到了具体要点击的位置(以上代码可以体现)。这样的做法简单粗暴,但缺点显而易见,就是受控端展示在主控端的画面是完全填充我们的SurfaceView的(类似ImageView的fitXY模式),可能会有不同程度的拉伸,但这并不影响我们精确地控制远程设备。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ithouse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值