import { MobileController, MobileState } from './MobileController'; import { RemoteViewport, MouseInput, TouchInput, TouchPoint, clamp, NetworkStatistics, Traffic, asciiKey } from './ControllerModels'; import { MobileTheme, MobileSegment, MobileIcon, MobileIconButton } from './MobileWidgets'; interface KeyRow { keys: string[]; } @Component export struct MobileRemoteView { @ObjectLink @Watch('stateChanged') state: MobileState; controller?: MobileController; @State private viewWidth: number = 1; @State private viewHeight: number = 1; @State private videoWidth: number = 1; @State private videoHeight: number = 1; @State private videoLeft: number = 0; @State private videoTop: number = 0; @State private zoom: number = 1; @State private cursorX: number = 0; @State private cursorY: number = 0; @State private orbX: number = 12; @State private orbY: number = 100; @State private menu: string = ''; @State private menuHeight: number = 390; @State private keyboardHeight: number = 220; private orbPositioned: boolean = false; @State private keyboard: boolean = false; @State private computerKeyboard: boolean = false; @State private shift: boolean = false; @State private modifiers: number[] = []; @State private keyboardX: number = 8; @State private keyboardY: number = 150; @State private mouseVisible: boolean = false; @State private leftHeld: boolean = false; @State private rightHeld: boolean = false; @State private wheelOffset: number = 0; private surface: XComponentController = new XComponentController(); private viewport: RemoteViewport = new RemoteViewport(); private mouse: MouseInput = new MouseInput((x: number, y: number, flag: number, scroll: number) => { this.controller?.pointer(x, y, flag, scroll); this.updateCursor(); }); private touches: TouchInput = new TouchInput(this.viewport, this.mouse); private longTimer: number = -1; private lastReset: number = -1; private lastWidth: number = 0; private lastHeight: number = 0; private lastRelative: boolean = true; private orbStartX: number = 0; private orbStartY: number = 0; private originX: number = 0; private originY: number = 0; private orbMoved: boolean = false; private barX: number = 0; private barY: number = 0; private wheelY: number = 0; private wheelLast: number = 0; private keyboardOriginX: number = 0; private keyboardOriginY: number = 0; aboutToAppear(): void { if (this.controller) this.controller.releaseInput = () => this.release(); this.stateChanged(); } aboutToDisappear(): void { this.release(); if (this.controller) this.controller.releaseInput = () => {}; this.controller?.surface('0'); } private stateChanged(): void { if (this.lastReset !== this.state.viewportReset || this.lastRelative !== this.state.relative) { this.release(); if (this.lastReset !== this.state.viewportReset) { this.viewport.reset(); this.applyViewport(); } this.lastReset = this.state.viewportReset; this.lastRelative = this.state.relative; } this.touches.relative = this.state.relative; if (this.lastWidth !== this.state.width || this.lastHeight !== this.state.height) { this.lastWidth = this.state.width; this.lastHeight = this.state.height; this.fit(); } } private release(): void { this.touches.cancel(); this.leftHeld = this.rightHeld = false; this.modifiers = []; this.wheelOffset = 0; if (this.longTimer >= 0) clearTimeout(this.longTimer); this.longTimer = -1; } private fit(): void { this.viewport.fit(this.viewWidth, this.viewHeight, this.state.width, this.state.height); this.applyViewport(); } private applyViewport(): void { this.videoWidth = this.viewport.baseWidth; this.videoHeight = this.viewport.baseHeight; this.zoom = this.viewport.scale; this.videoLeft = this.viewport.baseLeft + this.viewport.offsetX; this.videoTop = this.viewport.baseTop + this.viewport.offsetY; this.updateCursor(); } private updateCursor(): void { this.cursorX = this.viewport.left() + this.mouse.x * this.viewport.width(); this.cursorY = this.viewport.top() + this.mouse.y * this.viewport.height(); } private touch(event: TouchEvent): void { if (event.source === SourceType.Mouse) return; const kind = event.type === TouchType.Down ? 'down' : event.type === TouchType.Move ? 'move' : event.type === TouchType.Up ? 'up' : 'cancel'; if (kind === 'down') { this.touches.begin(); if (this.longTimer >= 0) clearTimeout(this.longTimer); if (event.touches.length === 1) this.longTimer = setTimeout(() => this.touches.hold(), 500); } if (kind === 'up' || kind === 'cancel' || event.touches.length > 1) { if (this.longTimer >= 0) clearTimeout(this.longTimer); this.longTimer = -1; } this.touches.touch(event.touches.map((t: TouchObject): TouchPoint => ({ x: t.x, y: t.y })), kind); this.applyViewport(); } private physicalMouse(event: MouseEvent): void { if (!this.viewport.contains(event.x, event.y) && event.action !== MouseAction.Release) return; this.mouse.move(this.viewport.x(event.x), this.viewport.y(event.y)); const button = event.button === MouseButton.Left ? 0 : event.button === MouseButton.Right ? 1 : event.button === MouseButton.Middle ? 2 : -1; if (button >= 0 && (event.action === MouseAction.Press || event.action === MouseAction.Release)) this.controller?.pointer(this.mouse.x, this.mouse.y, (event.action === MouseAction.Press ? 1 : 2) + button * 2, 0); } private orbTouch(event: TouchEvent): void { const p = event.changedTouches[0]; if (!p) return; if (event.type === TouchType.Down) { this.release(); this.orbStartX = p.screenX; this.orbStartY = p.screenY; this.originX = this.orbX; this.originY = this.orbY; this.orbMoved = false; } if (event.type === TouchType.Move) { const dx = p.screenX - this.orbStartX, dy = p.screenY - this.orbStartY; if (Math.hypot(dx, dy) > 4) { this.orbMoved = true; this.menu = ''; } if (this.orbMoved) { this.orbX = clamp(this.originX + dx, 8, this.viewWidth - 62); this.orbY = clamp(this.originY + dy, 8, this.viewHeight - 62); } } if (event.type === TouchType.Up && !this.orbMoved) this.menu = this.menu ? '' : 'controls'; } private barTouch(event: TouchEvent, button: number): void { const p = event.changedTouches[0]; if (!p) return; if (event.type === TouchType.Down) { this.barX = p.screenX; this.barY = this.wheelY = this.wheelLast = p.screenY; if (button !== 1) this.mouse.button(button === 0, true); } else if (event.type === TouchType.Move) { if (button === 1) { const delta = p.screenY - this.wheelY; this.wheelOffset = ((p.screenY - this.wheelLast) % 8 + 8) % 8; if (Math.abs(delta) > 8) { this.mouse.scroll(delta); this.wheelY = p.screenY; } } else this.mouse.drag(p.screenX - this.barX, p.screenY - this.barY, this.viewport.width(), this.viewport.height()); this.barX = p.screenX; this.barY = p.screenY; } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { if (button !== 1) this.mouse.button(button === 0, false); this.wheelOffset = 0; } this.leftHeld = this.mouse.left; this.rightHeld = this.mouse.right; } private action(index: number): void { if (index === 0) { this.keyboard = !this.keyboard; this.menu = ''; this.modifiers = []; if (this.keyboard) { this.keyboardX = (this.viewWidth - this.keyboardWidth()) / 2; this.keyboardY = Math.max(8, this.viewHeight - this.keyboardHeight - 20); } } if (index === 1) this.controller?.setRelative(!this.state.relative); if (index === 2) { this.release(); this.mouseVisible = !this.mouseVisible; this.menu = ''; } if (index === 3) { this.state.modal = 'displays'; this.menu = ''; } if (index === 4) this.menu = 'video'; if (index === 5) this.controller?.mute(); if (index === 6) this.controller?.chooseFile(); if (index === 7) this.menu = 'network'; if (index === 8) this.controller?.sas(); if (index === 9) { this.state.modal = 'clipboard'; this.menu = ''; } if (index === 10) this.controller?.togglePrivacy(); } private labels(): string[] { return ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板', this.state.privacyButtonTitle]; } private controlIds(): number[] { return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter((index: number) => (index !== 8 || this.state.platform === 'windows') && (index !== 10 || this.state.privacyVisible)); } private icons(): string[] { return ['keyboard', 'mouse', 'mouse', 'display', 'sliders', this.state.muted ? 'muted' : 'audio', 'folder', 'chart', 'lock', 'clipboard', this.state.privacyEnabled ? 'eye_off' : 'eye']; } private columns(): number { return this.viewHeight < 400 && this.viewWidth >= 400 ? 4 : 3; } private menuWidth(): number { return Math.max(1, Math.min(this.viewWidth - 16, this.columns() === 4 ? 380 : Math.max(280, Math.min(340, this.viewWidth - 160)))); } private menuX(): number { return clamp(this.orbX > this.viewWidth / 2 ? this.orbX - this.menuWidth() - 12 : this.orbX + 66, 8, this.viewWidth - this.menuWidth() - 8); } private mousePosition(): Position { let x = this.orbX >= 154 ? this.orbX - 146 : this.orbX + 60, y = this.orbY + 3; if (x + 140 > this.viewWidth - 8) { x = this.orbX; y = this.orbY + 60; if (y + 48 > this.viewHeight - 8) y = this.orbY - 54; } if (this.keyboard && x < this.keyboardX + this.keyboardWidth() && x + 140 > this.keyboardX && y < this.keyboardY + this.keyboardHeight && y + 48 > this.keyboardY) { y = this.keyboardY - 54; if (y < 8) y = this.keyboardY + this.keyboardHeight + 6; } return { x: clamp(x, 8, this.viewWidth - 148), y: clamp(y, 8, this.viewHeight - 56) }; } private keyboardWidth(): number { return Math.max(1, Math.min(this.computerKeyboard ? 520 : 440, this.viewWidth - 16)); } private keyboardRows(): KeyRow[] { return this.computerKeyboard ? [ { keys: ['Esc', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6'] }, { keys: ['Tab', 'Ctrl', 'Alt', 'Win', '↑', '⌫'] }, { keys: ['复制', '粘贴', '全选', '←', '↓', '→'] }, { keys: ['ABC', '空格', 'Enter'] } ] : [{ keys: ['1','2','3','4','5','6','7','8','9','0'] }, { keys: ['q','w','e','r','t','y','u','i','o','p'] }, { keys: ['a','s','d','f','g','h','j','k','l'] }, { keys: ['⇧','z','x','c','v','b','n','m','⌫'] }, { keys: ['电脑','剪贴板','空格','.','Enter'] }]; } private keyboardKey(value: string): void { if (value === '⇧') { this.shift = !this.shift; return; } if (value === '电脑' || value === 'ABC') { this.computerKeyboard = value === '电脑'; return; } if (value === '剪贴板') { this.state.modal = 'clipboard'; return; } const modifier = value === 'Ctrl' ? 17 : value === 'Alt' ? 18 : value === 'Win' ? 91 : 0; if (modifier) { this.modifiers = this.modifiers.includes(modifier) ? this.modifiers.filter((m: number) => m !== modifier) : [...this.modifiers, modifier]; return; } if (value === '复制' || value === '粘贴' || value === '全选') { this.controller?.shortcut(value === '复制' ? 67 : value === '粘贴' ? 86 : 65); return; } const names = ['空格','⌫','Enter','Esc','Tab','↑','↓','←','→']; const codes = [32,8,13,27,9,38,40,37,39]; let code = names.includes(value) ? codes[names.indexOf(value)] : /^F[1-6]$/.test(value) ? 111 + Number(value.substring(1)) : 0; if (!code && this.modifiers.length) { const key = asciiKey(this.shift ? value.toUpperCase() : value); code = key & 255; if (key & 256) this.modifiers = [...this.modifiers, 16]; } if (code) { this.controller?.chord(code, this.modifiers); this.modifiers = []; } else this.controller?.text(this.shift ? value.toUpperCase() : value); } private keyboardDrag(event: GestureEvent): void { this.keyboardX = clamp(this.keyboardOriginX + event.offsetX, 8, this.viewWidth - this.keyboardWidth() - 8); this.keyboardY = clamp(this.keyboardOriginY + event.offsetY, 8, Math.max(8, this.viewHeight - this.keyboardHeight - 8)); } @Builder NetworkRow(title: string, traffic: Traffic | undefined) { Row({ space: 4 }) { Text(title).fontSize(11).fontWeight(title === '合计' ? 600 : 400).fontColor(MobileTheme.ink).width(28) Text(NetworkStatistics.rate(traffic?.inbound ?? -1)).fontSize(11).minFontSize(8).maxLines(1).layoutWeight(1).textAlign(TextAlign.End) Text(NetworkStatistics.rate(traffic?.outbound ?? -1)).fontSize(11).minFontSize(8).maxLines(1).layoutWeight(1).textAlign(TextAlign.End) Text(NetworkStatistics.loss(traffic?.loss ?? -1)).fontSize(11).width(46).textAlign(TextAlign.End) }.padding({ left: 4, right: 4 }).height(26).width('100%').borderRadius(5).backgroundColor(title === '合计' ? '#122E7ADB' : Color.Transparent) } @Builder NetworkDetail(label: string, value: string, encrypted: boolean = false) { Row({ space: 4 }) { Text(label).fontSize(12).fontColor(MobileTheme.secondary).layoutWeight(1) if (encrypted) { MobileIcon({ name: 'lock', iconExtent: 14, color: MobileTheme.green }) } Text(value).fontSize(12).fontColor(encrypted ? MobileTheme.green : MobileTheme.ink).textAlign(TextAlign.End) }.width('100%').constraintSize({ minHeight: 21 }) } @Builder NetworkCodec(label: string, value: string) { Row({ space: 8 }) { Text(label).fontSize(12).fontColor(MobileTheme.secondary).maxLines(1) Text(value).fontSize(12).fontColor(MobileTheme.ink).textAlign(TextAlign.End).layoutWeight(1) .maxLines(1).textOverflow({ overflow: TextOverflow.MARQUEE }) .marqueeOptions({ start: true, step: 0.5, loop: -1, fromStart: true }) }.width('100%').constraintSize({ minHeight: 21 }) } @Builder Menu() { Column({ space: 10 }) { Row({ space: 6 }) { if (this.menu !== 'controls') { MobileIconButton({ icon: 'back', label: '返回控制栏', iconExtent: 30, iconSize: 19, action: () => this.menu = 'controls' }) } Row({ space: 6 }) { if (this.menu === 'controls') { Row() {}.width(6).height(6).borderRadius(3).backgroundColor(MobileTheme.green) } Text(this.menu === 'controls' ? '已连接' : this.menu === 'video' ? '画面设置' : '网络状态').fontSize(14).fontWeight(600).fontColor(MobileTheme.ink) }.layoutWeight(1) MobileIconButton({ icon: 'close', label: '关闭控制栏', iconExtent: 30, iconSize: 20, action: () => this.menu = '' }) }.width('100%') Scroll() { Column({ space: 8 }) { if (this.menu === 'controls') { GridRow({ columns: this.columns(), gutter: 7 }) { ForEach(this.controlIds(), (index: number) => { GridCol() { Column({ space: 4 }) { MobileIcon({ name: this.icons()[index], iconExtent: 19 }) Text(this.labels()[index]).fontSize(10).fontColor(MobileTheme.ink).maxLines(1) } .height(52).width('100%').justifyContent(FlexAlign.Center) .backgroundColor('#F0F0F2').borderRadius(10) .enabled(index !== 10 || this.state.ready && this.state.privacyCanToggle) .opacity(index === 10 && (!this.state.ready || !this.state.privacyCanToggle) ? 0.45 : 1) .accessibilitySelected(index === 10 && this.state.privacyEnabled) .accessibilityText(index === 10 ? `${this.labels()[index]},${this.state.privacyStatus}` : this.labels()[index]).accessibilityLevel('yes').onClick(() => this.action(index)) } }, (index: number) => index.toString()) } if (this.state.privacyFeedback) { Text(this.state.privacyFeedback).fontSize(11).fontColor(MobileTheme.secondary).width('100%') } if (this.state.transfer) { Text(this.state.transfer).fontSize(11).fontColor(MobileTheme.secondary).width('100%') } if (this.state.receivedPath) { Button('保存收到的文件', { type: ButtonType.Normal }).fontSize(14).fontColor(MobileTheme.blue).backgroundColor(Color.White).height(42).width('100%').onClick(() => this.controller?.saveFile()) } } else if (this.menu === 'video') { Text(this.state.videoFeedback).fontSize(12).fontColor(MobileTheme.secondary).width('100%') Text('画面质量').fontSize(12).width('100%') MobileSegment({ labels: ['低', '中', '高'], selection: this.state.videoQuality, available: this.state.videoSupported, changed: (i: number) => this.controller?.updateVideo(0, i) }) Text('画面采集帧率').fontSize(12).width('100%') MobileSegment({ labels: ['30 fps', '60 fps'], selection: this.state.videoRate === 60 ? 1 : 0, available: this.state.videoSupported, changed: (i: number) => this.controller?.updateVideo(1, i ? 60 : 30) }) Text('画面偏好').fontSize(12).width('100%') MobileSegment({ labels: ['帧率优先', '画质优先', '平衡'], selection: this.state.videoSelectedPreference, available: this.state.videoSupported, changed: (i: number) => this.controller?.updateVideo(2, i) }) } else { Row({ space: 4 }) { Text('').width(28) Text('接收').fontSize(11).fontColor(MobileTheme.secondary).layoutWeight(1).textAlign(TextAlign.End) Text('发送').fontSize(11).fontColor(MobileTheme.secondary).layoutWeight(1).textAlign(TextAlign.End) Text('丢包率').fontSize(11).fontColor(MobileTheme.secondary).width(46).textAlign(TextAlign.End) }.padding({ left: 4, right: 4 }).height(24).width('100%') Column() { this.NetworkRow('视频', this.controller?.network.current(this.state.networkTime)?.video) this.NetworkRow('音频', this.controller?.network.current(this.state.networkTime)?.audio) this.NetworkRow('数据', this.controller?.network.current(this.state.networkTime)?.data) this.NetworkRow('合计', this.controller?.network.current(this.state.networkTime)?.total) }.width('100%') Divider().strokeWidth(0.5).color(MobileTheme.line).margin({ top: 2, bottom: 2 }) this.NetworkDetail('帧率', `${this.controller?.network.fps(this.state.networkTime) ?? '—'} FPS`) this.NetworkDetail('分辨率', this.state.hasFrame ? `${this.state.width} × ${this.state.height}` : '—') this.NetworkDetail('画面延时', NetworkStatistics.delay(this.controller?.network.latency(this.state.networkTime) ?? -1)) this.NetworkDetail('连接延时(RTT)', NetworkStatistics.delay(this.state.networkTime - (this.controller?.network.rttTime ?? 0) < 3000 ? this.controller?.network.rtt ?? -1 : -1)) this.NetworkDetail('连接方式', this.controller?.network.mode(this.state.networkTime) ?? '—') this.NetworkDetail('媒体加密', this.controller?.network.current(this.state.networkTime) === undefined ? '—' : this.controller?.network.report?.srtp ? 'SRTP 已启用' : '未启用', this.controller?.network.current(this.state.networkTime)?.srtp === true) this.NetworkCodec('被控端编码器', this.controller?.network.remoteEncoder || '—') this.NetworkCodec('控制端解码器', this.controller?.network.report?.decoder || '—') } }.width('100%') }.constraintSize({ maxHeight: Math.max(48, this.viewHeight - 132) }).width('100%').scrollBar(BarState.Off) Button('断开连接', { type: ButtonType.Normal }).fontSize(14).fontColor(Color.White).width('100%').height(34).borderRadius(8).backgroundColor(MobileTheme.red) .onClick(() => { this.release(); this.state.modal = 'disconnect'; this.menu = ''; this.keyboard = false; }) }.padding(12).width(this.menuWidth()).backgroundColor(Color.White).borderRadius(18) .border({ width: 0.5, color: MobileTheme.line }).shadow({ radius: 24, color: '#33000000', offsetY: 6 }) .onAreaChange((_old: Area, next: Area) => this.menuHeight = Number(next.height)) .position({ x: this.menuX(), y: clamp(this.orbY, 8, Math.max(8, this.viewHeight - this.menuHeight - 8)) }) } @Builder Keyboard() { Column({ space: 3 }) { Row() { Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }.layoutWeight(1).height(28).accessibilityText('拖动键盘') .gesture(PanGesture().onActionStart(() => { this.keyboardOriginX = this.keyboardX; this.keyboardOriginY = this.keyboardY; }) .onActionUpdate((e: GestureEvent) => this.keyboardDrag(e))) MobileIconButton({ icon: 'close', label: '收起键盘', iconExtent: 28, iconSize: 18, action: () => { this.keyboard = false; this.modifiers = []; } }) }.width('100%') Scroll() { Column({ space: 3 }) { ForEach(this.keyboardRows(), (row: KeyRow) => { Row({ space: 3 }) { ForEach(row.keys, (key: string) => { Text(this.shift && key.length === 1 ? key.toUpperCase() : key).fontSize(key.length > 2 ? 11 : 14) .height(31).layoutWeight(key === '空格' ? 3 : 1).textAlign(TextAlign.Center).borderRadius(5).fontColor(MobileTheme.ink) .shadow({ radius: 0.5, color: '#33000000', offsetY: 1 }) .backgroundColor((key === '⇧' && this.shift) || this.modifiers.includes(key === 'Ctrl' ? 17 : key === 'Alt' ? 18 : key === 'Win' ? 91 : -1) ? '#9BC7F7' : key.length > 1 || key === '⇧' ? '#AFB8C5' : '#FFFFFF') .accessibilityText(key).accessibilityLevel('yes').onClick(() => this.keyboardKey(key)) }, (key: string) => key) }.width('100%') }) }.width('100%') }.width('100%').constraintSize({ maxHeight: Math.max(31, this.viewHeight - 60) }).scrollBar(BarState.Off) }.padding(5).width(this.keyboardWidth()).backgroundColor('#D1D6DE').borderRadius(12) .shadow({ radius: 20, color: '#33000000', offsetY: 5 }).onAreaChange((_old: Area, next: Area) => this.keyboardHeight = Number(next.height)) .position({ x: clamp(this.keyboardX, 8, this.viewWidth - this.keyboardWidth() - 8), y: clamp(this.keyboardY, 8, Math.max(8, this.viewHeight - this.keyboardHeight - 8)) }) } @Builder MouseBar() { Row({ space: 2 }) { ForEach([0, 1, 2], (button: number) => { Stack() { if (button === 1) { Stack() { Column({ space: 4 }) { ForEach([0, 1, 2, 3, 4, 5, 6], (line: number) => { Row() {}.height(3).width(14).backgroundColor('#737378') }) }.translate({ y: this.wheelOffset - 8 }) }.width(18).height(30).clip(true) } else { MobileIcon({ name: button === 0 ? 'mouse_left' : 'mouse_right', iconExtent: 22 }) } }.width(44).height(44).borderRadius(7).clip(true) .backgroundColor(button === 0 && this.leftHeld || button === 2 && this.rightHeld ? '#9BC7F7' : '#E9E9EE') .accessibilityText(button === 0 ? '鼠标左键,按住拖动' : button === 1 ? '滚轮,上下滑动' : '鼠标右键,按住拖动') .onTouch((event: TouchEvent) => this.barTouch(event, button)) }) }.padding(2).width(140).height(48).backgroundColor('#FFFFFF').borderRadius(10) .border({ width: 0.5, color: MobileTheme.line }).shadow({ radius: 12, color: '#33000000', offsetY: 4 }).position(this.mousePosition()) } build() { Stack({ alignContent: Alignment.TopStart }) { XComponent({ id: 'mobileVideo', type: XComponentType.SURFACE, controller: this.surface }) .width(this.videoWidth).height(this.videoHeight).position({ x: this.videoLeft, y: this.videoTop }) .scale({ x: this.zoom, y: this.zoom }).onLoad(() => this.controller?.surface(this.surface.getXComponentSurfaceId(), this.surface)) .onDestroy(() => this.controller?.surface('0')) if (!this.state.hasFrame) { Column({ space: 12 }) { LoadingProgress().width(32).height(32).color(Color.White) Text('等待远端画面…').fontSize(14).fontColor(Color.White) }.padding(20).constraintSize({ minWidth: 200 }).backgroundColor('#B0202024').borderRadius(14) .position({ x: (this.viewWidth - 200) / 2, y: (this.viewHeight - 104) / 2 }).hitTestBehavior(HitTestMode.None) } Column() {}.width('100%').height('100%').focusable(true).defaultFocus(true) .onTouch((e: TouchEvent) => this.touch(e)).onMouse((e: MouseEvent) => this.physicalMouse(e)) .onAxisEvent((event: AxisEvent) => { const delta = event.getVerticalAxisValue(); if (delta) this.mouse.scroll(-delta); }) .onKeyEvent((e: KeyEvent) => { if (e.type === KeyType.Down || e.type === KeyType.Up) { this.controller?.physicalKey(e.keyCode, e.type === KeyType.Down); return true; } return false; }) .onBlur(() => this.controller?.release()) .accessibilityText('远程桌面:轻触点击、长按右键、双指捏合缩放、放大后双指拖动画面,原始比例下双指滚动') MobileIcon({ name: 'cursor', iconExtent: 24, color: '#000000' }) .position({ x: this.cursorX - 1, y: this.cursorY - 1 }).hitTestBehavior(HitTestMode.None) if (this.menu) { Column() {}.width('100%').height('100%').onClick(() => this.menu = ''); this.Menu() } if (this.keyboard) { this.Keyboard() } if (this.mouseVisible && !this.menu && this.state.hasFrame) { this.MouseBar() } Stack() { Image($r('app.media.mobile_brand')).width(46).height(46).objectFit(ImageFit.Cover).borderRadius(23).draggable(false).hitTestBehavior(HitTestMode.None) }.width(54).height(54).backgroundColor('#F5F5F7').borderRadius(27).shadow({ radius: 14, color: '#33000000', offsetY: 4 }) .position({ x: this.orbX, y: this.orbY }).accessibilityText('远程控制菜单,拖动可移动').accessibilityLevel('yes') .onTouch((e: TouchEvent) => this.orbTouch(e)) }.width('100%').height('100%').backgroundColor(Color.Black).clip(true) .onAreaChange((_old: Area, next: Area) => { this.release(); this.viewWidth = Number(next.width); this.viewHeight = Number(next.height); this.fit(); if (!this.orbPositioned && this.viewWidth > 100) { this.orbX = this.viewWidth - 66; this.orbY = 17; this.orbPositioned = true; } this.orbX = clamp(this.orbX, 8, this.viewWidth - 62); this.orbY = clamp(this.orbY, 8, this.viewHeight - 62); this.keyboardX = clamp(this.keyboardX, 8, this.viewWidth - this.keyboardWidth() - 8); }) } }