From f5f496f1cf593a148e133669d356de11519b2278 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 16 Feb 2022 23:08:23 +0800 Subject: [PATCH] add custom gestures --- lib/gestures.dart | 770 +++++++++++++++++++++++++++++++++++++++++++ lib/home_page.dart | 2 +- lib/remote_page.dart | 62 +++- lib/server_page.dart | 59 ++-- 4 files changed, 860 insertions(+), 33 deletions(-) create mode 100644 lib/gestures.dart diff --git a/lib/gestures.dart b/lib/gestures.dart new file mode 100644 index 000000000..57862bb93 --- /dev/null +++ b/lib/gestures.dart @@ -0,0 +1,770 @@ +import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +enum CustomTouchGestureState { + none, + oneFingerPan, + twoFingerScale, + twoFingerVerticalDrag, + twoFingerHorizontalDrag +} + +const kScaleSlop = kPrecisePointerPanSlop / 10; + +class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { + CustomTouchGestureRecognizer({ + Object debugOwner, + Set supportedDevices, + }) : super( + debugOwner: debugOwner, + supportedDevices: supportedDevices, + ) { + _init(); + } + + // oneFingerPan + GestureDragStartCallback onOneFingerPanStart; + GestureDragUpdateCallback onOneFingerPanUpdate; + GestureDragEndCallback onOneFingerPanEnd; + + // twoFingerScale + GestureScaleStartCallback onTwoFingerScaleStart; + GestureScaleUpdateCallback onTwoFingerScaleUpdate; + GestureScaleEndCallback onTwoFingerScaleEnd; + + // twoFingerVerticalDrag + GestureDragStartCallback onTwoFingerVerticalDragStart; + GestureDragUpdateCallback onTwoFingerVerticalDragUpdate; + GestureDragEndCallback onTwoFingerVerticalDragEnd; + + // twoFingerHorizontalDrag + GestureDragStartCallback onTwoFingerHorizontalDragStart; + GestureDragUpdateCallback onTwoFingerHorizontalDragUpdate; + GestureDragEndCallback onTwoFingerHorizontalDragEnd; + + void _init() { + debugPrint("CustomTouchGestureRecognizer init"); + onStart = (d) { + if (d.pointerCount == 1) { + _currentState = CustomTouchGestureState.oneFingerPan; + debugPrint("start pan"); + } else if (d.pointerCount == 2) { + _currentState = CustomTouchGestureState.none; + startWatchTimer(); + } else { + _currentState = CustomTouchGestureState.none; + _reset(); + } + }; + onUpdate = (d) { + if (_isWatch) { + _updateCompute(d); + return; + } + if (_currentState != CustomTouchGestureState.none) { + switch (_currentState) { + case CustomTouchGestureState.oneFingerPan: + if (onOneFingerPanUpdate != null) { + onOneFingerPanUpdate(_getDragUpdateDetails(d)); + } + break; + case CustomTouchGestureState.twoFingerScale: + if (onTwoFingerScaleUpdate != null) { + onTwoFingerScaleUpdate(d); + } + break; + case CustomTouchGestureState.twoFingerHorizontalDrag: + if (onTwoFingerHorizontalDragUpdate != null) { + onTwoFingerHorizontalDragUpdate(_getDragUpdateDetails(d)); + } + break; + case CustomTouchGestureState.twoFingerVerticalDrag: + if (onTwoFingerVerticalDragUpdate != null) { + onTwoFingerVerticalDragUpdate(_getDragUpdateDetails(d)); + } + break; + default: + break; + } + return; + } + }; + onEnd = (d) { + debugPrint("ScaleGestureRecognizer onEnd"); + // end + switch (_currentState) { + case CustomTouchGestureState.oneFingerPan: + debugPrint("TwoFingerState.pan onEnd"); + if (onOneFingerPanEnd != null) { + onOneFingerPanEnd(_getDragEndDetails(d)); + } + break; + case CustomTouchGestureState.twoFingerScale: + debugPrint("TwoFingerState.scale onEnd"); + if (onTwoFingerScaleEnd != null) { + onTwoFingerScaleEnd(d); + } + break; + case CustomTouchGestureState.twoFingerHorizontalDrag: + debugPrint("TwoFingerState.horizontal onEnd"); + if (onTwoFingerHorizontalDragEnd != null) { + onTwoFingerHorizontalDragEnd(_getDragEndDetails(d)); + } + break; + case CustomTouchGestureState.twoFingerVerticalDrag: + debugPrint("TwoFingerState.vertical onEnd"); + if (onTwoFingerVerticalDragEnd != null) { + onTwoFingerVerticalDragEnd(_getDragEndDetails(d)); + } + break; + default: + break; + } + _currentState = CustomTouchGestureState.none; + _reset(); + }; + } + + var _currentState = CustomTouchGestureState.none; + var _isWatch = false; + + Timer _timer; + double _sumScale = 0; + double _sumVertical = 0; + double _sumHorizontal = 0; + + void _clearSum() { + _sumScale = 0; + _sumVertical = 0; + _sumHorizontal = 0; + } + + void _reset() { + _isWatch = false; + _clearSum(); + if (_timer != null) _timer.cancel(); + } + + void _updateCompute(ScaleUpdateDetails d) { + _sumScale += d.scale - 1; + _sumHorizontal += d.focalPointDelta.dx; + _sumVertical += d.focalPointDelta.dy; + // start + if (_sumScale.abs() > kScaleSlop) { + debugPrint("start Scale"); + _currentState = CustomTouchGestureState.twoFingerScale; + if (onOneFingerPanStart != null) { + onOneFingerPanStart(_getDragStartDetails(d)); + } + _reset(); + } else if (_sumHorizontal.abs() > kPrecisePointerPanSlop) { + debugPrint("start Horizontal"); + _currentState = CustomTouchGestureState.twoFingerHorizontalDrag; + if (onTwoFingerHorizontalDragUpdate != null) { + onTwoFingerHorizontalDragUpdate(_getDragUpdateDetails(d)); + } + _reset(); + } else if (_sumVertical.abs() > kPrecisePointerPanSlop) { + debugPrint("start Vertical"); + if (onTwoFingerVerticalDragStart != null) { + _getDragStartDetails(d); + } + _currentState = CustomTouchGestureState.twoFingerVerticalDrag; + _reset(); + } + } + + void startWatchTimer() { + debugPrint("startWatchTimer"); + _isWatch = true; + _clearSum(); + if (_timer != null) _timer.cancel(); + _timer = Timer(const Duration(milliseconds: 200), _reset); + } + + DragStartDetails _getDragStartDetails(ScaleUpdateDetails d) => + DragStartDetails( + globalPosition: d.focalPoint, + localPosition: d.localFocalPoint, + ); + + DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) => + DragUpdateDetails( + globalPosition: d.focalPoint, + localPosition: d.localFocalPoint, + delta: d.focalPointDelta); + + DragEndDetails _getDragEndDetails(ScaleEndDetails d) => + DragEndDetails(velocity: d.velocity); +} + +class HoldTapMoveGestureRecognizer extends GestureRecognizer { + HoldTapMoveGestureRecognizer({ + Object debugOwner, + Set supportedDevices, + }) : super( + debugOwner: debugOwner, + supportedDevices: supportedDevices, + ); + + GestureDragStartCallback onHoldDragStart; + GestureDragUpdateCallback onHoldDragUpdate; + GestureDragDownCallback onHoldDragDown; + GestureDragCancelCallback onHoldDragCancel; + + bool _isStart = false; + + Timer _firstTapUpTimer; // 第一次点击后的计时 超时未等到第二次操作则reject + Timer _secondTapDownTimer; // 第二次点击后的计时 期间内有其他的操作则reject 超时则判定成功 drag update + _TapTracker _firstTap; + _TapTracker _secondTap; + + final Map _trackers = {}; + + @override + bool isPointerAllowed(PointerDownEvent event) { + if (_firstTap == null) { + switch (event.buttons) { + case kPrimaryButton: + if (onHoldDragStart == null && + onHoldDragUpdate == null && + onHoldDragCancel == null) { + return false; + } + break; + default: + return false; + } + } + return super.isPointerAllowed(event); + } + + @override + void addAllowedPointer(PointerDownEvent event) { + // 检测按下事件 + if (_firstTap != null) { + if (!_firstTap.isWithinGlobalTolerance(event, kDoubleTapSlop)) { + // Ignore out-of-bounds second taps. + return; + } else if (!_firstTap.hasElapsedMinTime() || + !_firstTap.hasSameButton(event)) { + // Restart when the second tap is too close to the first (touch screens + // often detect touches intermittently), or when buttons mismatch. + _reset(); + return _trackTap(event); + } else if (onHoldDragDown != null) { + invokeCallback( + 'onHoldDragDown', + () => onHoldDragDown(DragDownDetails( + globalPosition: event.position, + localPosition: event.localPosition))); + } + } + _trackTap(event); // 捕捉第一次tap + } + + void _trackTap(PointerDownEvent event) { + _stopFirstTapUpTimer(); + _stopSecondTapDownTimer(); + final _TapTracker tracker = _TapTracker( + event: event, + entry: GestureBinding.instance.gestureArena.add(event.pointer, this), + doubleTapMinTime: kDoubleTapMinTime, + gestureSettings: gestureSettings, + ); + _trackers[event.pointer] = tracker; + tracker.startTrackingPointer(_handleEvent, event.transform); + } + + // 实际的逻辑应该是第二次down后一段时间没有抬起则表示start 刚好是双击取反 + void _handleEvent(PointerEvent event) { + final _TapTracker tracker = _trackers[event.pointer]; + if (event is PointerUpEvent) { + if (_firstTap == null && _secondTap == null) { + _registerFirstTap(tracker); + } else { + // 检测到其他的抬起事件则取消 + _reject(tracker); + } + } else if (event is PointerDownEvent) { + if (_firstTap != null && _secondTap == null) { + _registerSecondTap(tracker); + } + } else if (event is PointerMoveEvent) { + // 检测到first tap move 则取消,检测到second tap move且已经通过竞技场则update + if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) { + if (_firstTap != null && _firstTap.pointer == event.pointer) { + // first tap move + _reject(tracker); + } else if (_secondTap != null && _secondTap.pointer == event.pointer) { + // debugPrint("_secondTap move"); + // second tap move + if (!_isStart) { + _resolve(); + } + if (onHoldDragUpdate != null) + onHoldDragUpdate(DragUpdateDetails( + globalPosition: event.position, + localPosition: event.localPosition, + delta: event.delta)); + } + } + } else if (event is PointerCancelEvent) { + _reject(tracker); + } + } + + @override + void acceptGesture(int pointer) {} + + @override + void rejectGesture(int pointer) { + _TapTracker tracker = _trackers[pointer]; + // If tracker isn't in the list, check if this is the first tap tracker + if (tracker == null && _firstTap != null && _firstTap.pointer == pointer) { + tracker = _firstTap; + } + // If tracker is still null, we rejected ourselves already + if (tracker != null) { + _reject(tracker); + } + } + + void _resolve() { + _stopSecondTapDownTimer(); + _firstTap?.entry?.resolve(GestureDisposition.accepted); + _secondTap?.entry?.resolve(GestureDisposition.accepted); + _isStart = true; + // TODO start details + if (onHoldDragStart != null) onHoldDragStart(DragStartDetails()); + } + + void _reject(_TapTracker tracker) { + _checkCancel(); + + _isStart = false; + _trackers.remove(tracker.pointer); + tracker.entry.resolve(GestureDisposition.rejected); + _freezeTracker(tracker); + _reset(); + } + + @override + void dispose() { + _reset(); + super.dispose(); + } + + void _reset() { + _isStart = false; + // debugPrint("reset"); + _stopFirstTapUpTimer(); + _stopSecondTapDownTimer(); + if (_firstTap != null) { + if (_trackers.isNotEmpty) { + _checkCancel(); + } + // Note, order is important below in order for the resolve -> reject logic + // to work properly. + final _TapTracker tracker = _firstTap; + _firstTap = null; + _reject(tracker); + GestureBinding.instance.gestureArena.release(tracker.pointer); + + if (_secondTap != null) { + final _TapTracker tracker = _secondTap; + _secondTap = null; + _reject(tracker); + GestureBinding.instance.gestureArena.release(tracker.pointer); + } + } + // TODO 正确的释放资源 + _firstTap = null; + _secondTap = null; + _clearTrackers(); + } + + void _registerFirstTap(_TapTracker tracker) { + _startFirstTapUpTimer(); + GestureBinding.instance.gestureArena.hold(tracker.pointer); + // Note, order is important below in order for the clear -> reject logic to + // work properly. + _freezeTracker(tracker); + _trackers.remove(tracker.pointer); + _firstTap = tracker; + } + + void _registerSecondTap(_TapTracker tracker) { + // 清除first tap的状态 + if (_firstTap != null) { + _stopFirstTapUpTimer(); + _freezeTracker(_firstTap); + _firstTap = null; + } + + _startSecondTapDownTimer(); + GestureBinding.instance.gestureArena.hold(tracker.pointer); + + _secondTap = tracker; + + // TODO + } + + void _clearTrackers() { + _trackers.values.toList().forEach(_reject); + assert(_trackers.isEmpty); + } + + void _freezeTracker(_TapTracker tracker) { + tracker.stopTrackingPointer(_handleEvent); + } + + void _startFirstTapUpTimer() { + _firstTapUpTimer ??= Timer(kDoubleTapTimeout, _reset); + } + + void _startSecondTapDownTimer() { + _secondTapDownTimer ??= Timer(kDoubleTapTimeout, _resolve); + } + + void _stopFirstTapUpTimer() { + if (_firstTapUpTimer != null) { + _firstTapUpTimer.cancel(); + _firstTapUpTimer = null; + } + } + + void _stopSecondTapDownTimer() { + if (_secondTapDownTimer != null) { + _secondTapDownTimer.cancel(); + _secondTapDownTimer = null; + } + } + + void _checkCancel() { + if (onHoldDragCancel != null) { + invokeCallback('onHoldDragCancel', onHoldDragCancel); + } + } + + @override + String get debugDescription => 'double tap'; +} + +class DoubleFinerTapGestureRecognizer extends GestureRecognizer { + DoubleFinerTapGestureRecognizer({ + Object debugOwner, + Set supportedDevices, + }) : super( + debugOwner: debugOwner, + supportedDevices: supportedDevices, + ); + + GestureTapDownCallback onDoubleFinerTapDown; + GestureTapDownCallback onDoubleFinerTap; + GestureTapCancelCallback onDoubleFinerTapCancel; + + Timer _firstTapTimer; // 第一次点击后的计时 超时未等到第二次操作则reject + _TapTracker _firstTap; + + var _isStart = false; + + final Set _upTap = {}; + + final Map _trackers = {}; + + @override + bool isPointerAllowed(PointerDownEvent event) { + if (_firstTap == null) { + switch (event.buttons) { + case kPrimaryButton: + if (onDoubleFinerTapDown == null && + onDoubleFinerTap == null && + onDoubleFinerTapCancel == null) { + return false; + } + break; + default: + return false; + } + } + return super.isPointerAllowed(event); + } + + @override + void addAllowedPointer(PointerDownEvent event) { + // 检测按下事件 + debugPrint("addAllowedPointer"); + if (_isStart) { + // second + if (onDoubleFinerTapDown != null) { + final TapDownDetails details = TapDownDetails( + globalPosition: event.position, + localPosition: event.localPosition, + kind: getKindForPointer(event.pointer), + ); + invokeCallback( + 'onDoubleFinerTapDown', () => onDoubleFinerTapDown(details)); + } + } else { + // first tap + _isStart = true; + _startFirstTapDownTimer(); + } + _trackTap(event); // 捕捉tap + } + + void _trackTap(PointerDownEvent event) { + final _TapTracker tracker = _TapTracker( + event: event, + entry: GestureBinding.instance.gestureArena.add(event.pointer, this), + doubleTapMinTime: kDoubleTapMinTime, + gestureSettings: gestureSettings, + ); + _trackers[event.pointer] = tracker; + // debugPrint("_trackers:$_trackers"); + tracker.startTrackingPointer(_handleEvent, event.transform); + + _registerTap(tracker); + } + + // 实际的逻辑应该是第二次down后一段时间没有抬起则表示start 刚好是双击取反 + void _handleEvent(PointerEvent event) { + final _TapTracker tracker = _trackers[event.pointer]; + if (event is PointerUpEvent) { + debugPrint("PointerUpEvent"); + _upTap.add(tracker.pointer); + } else if (event is PointerMoveEvent) { + if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) + _reject(tracker); + } else if (event is PointerCancelEvent) { + _reject(tracker); + } + } + + @override + void acceptGesture(int pointer) {} + + @override + void rejectGesture(int pointer) { + _TapTracker tracker = _trackers[pointer]; + // If tracker isn't in the list, check if this is the first tap tracker + if (tracker == null && _firstTap != null && _firstTap.pointer == pointer) { + tracker = _firstTap; + } + // If tracker is still null, we rejected ourselves already + if (tracker != null) { + _reject(tracker); + } + } + + void _reject(_TapTracker tracker) { + _trackers.remove(tracker.pointer); + tracker.entry.resolve(GestureDisposition.rejected); + _freezeTracker(tracker); + if (_firstTap != null) { + if (tracker == _firstTap) { + _reset(); + } else { + _checkCancel(); + if (_trackers.isEmpty) { + _reset(); + } + } + } + } + + @override + void dispose() { + _reset(); + super.dispose(); + } + + void _reset() { + _stopFirstTapUpTimer(); + _firstTap = null; + // TODO 正确的释放资源 + _clearTrackers(); + } + + void _registerTap(_TapTracker tracker) { + GestureBinding.instance.gestureArena.hold(tracker.pointer); + // Note, order is important below in order for the clear -> reject logic to + // work properly. + } + + void _clearTrackers() { + _trackers.values.toList().forEach(_reject); + assert(_trackers.isEmpty); + } + + void _freezeTracker(_TapTracker tracker) { + tracker.stopTrackingPointer(_handleEvent); + } + + void _startFirstTapDownTimer() { + _firstTapTimer ??= Timer(kDoubleTapTimeout, _timeoutCheck); + } + + void _stopFirstTapUpTimer() { + if (_firstTapTimer != null) { + _firstTapTimer.cancel(); + _firstTapTimer = null; + } + } + + void _timeoutCheck() { + _isStart = false; + if (_upTap.length == 2) { + _resolve(); + } else { + _reset(); + } + _upTap.clear(); + } + + void _resolve() { + // TODO tap down details + if (onDoubleFinerTap != null) onDoubleFinerTap(TapDownDetails()); + _trackers.forEach((key, value) { + value.entry.resolve(GestureDisposition.accepted); + }); + _reset(); + } + + void _checkCancel() { + if (onDoubleFinerTapCancel != null) { + invokeCallback('onHoldDragCancel', onDoubleFinerTapCancel); + } + } + + @override + String get debugDescription => 'double tap'; +} + +/// TapTracker helps track individual tap sequences as part of a +/// larger gesture. +class _TapTracker { + _TapTracker({ + PointerDownEvent event, + this.entry, + Duration doubleTapMinTime, + this.gestureSettings, + }) : assert(doubleTapMinTime != null), + assert(event != null), + assert(event.buttons != null), + pointer = event.pointer, + _initialGlobalPosition = event.position, + initialButtons = event.buttons, + _doubleTapMinTimeCountdown = + _CountdownZoned(duration: doubleTapMinTime); + + final DeviceGestureSettings gestureSettings; + final int pointer; + final GestureArenaEntry entry; + final Offset _initialGlobalPosition; + final int initialButtons; + final _CountdownZoned _doubleTapMinTimeCountdown; + + bool _isTrackingPointer = false; + + void startTrackingPointer(PointerRoute route, Matrix4 transform) { + if (!_isTrackingPointer) { + _isTrackingPointer = true; + GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform); + } + } + + void stopTrackingPointer(PointerRoute route) { + if (_isTrackingPointer) { + _isTrackingPointer = false; + GestureBinding.instance.pointerRouter.removeRoute(pointer, route); + } + } + + bool isWithinGlobalTolerance(PointerEvent event, double tolerance) { + final Offset offset = event.position - _initialGlobalPosition; + return offset.distance <= tolerance; + } + + bool hasElapsedMinTime() { + return _doubleTapMinTimeCountdown.timeout; + } + + bool hasSameButton(PointerDownEvent event) { + return event.buttons == initialButtons; + } +} + +/// CountdownZoned tracks whether the specified duration has elapsed since +/// creation, honoring [Zone]. +class _CountdownZoned { + _CountdownZoned({Duration duration}) : assert(duration != null) { + Timer(duration, _onTimeout); + } + + bool _timeout = false; + + bool get timeout => _timeout; + + void _onTimeout() { + _timeout = true; + } +} + +RawGestureDetector getMixinGestureDetector({ + Widget child, + GestureTapUpCallback onTapUp, + GestureDoubleTapCallback onDoubleTap, + GestureDragStartCallback onHoldDragStart, + GestureDragUpdateCallback onHoldDragUpdate, + GestureDragCancelCallback onHoldDragCancel, + GestureTapDownCallback onDoubleFinerTap, + GestureDragStartCallback onOneFingerPanStart, + GestureDragUpdateCallback onOneFingerPanUpdate, + GestureScaleUpdateCallback onTwoFingerScaleUpdate, + GestureDragUpdateCallback onTwoFingerHorizontalDragUpdate, + GestureDragUpdateCallback onTwoFingerVerticalDragUpdate, +}) { + return RawGestureDetector( + child: child, + gestures: { + // Official + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), (instance) { + instance.onTapUp = onTapUp; + }), + DoubleTapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => DoubleTapGestureRecognizer(), (instance) { + instance.onDoubleTap = onDoubleTap; + }), + // Customized + HoldTapMoveGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => HoldTapMoveGestureRecognizer(), + (instance) => { + instance + ..onHoldDragStart = onHoldDragStart + ..onHoldDragUpdate = onHoldDragUpdate + ..onHoldDragCancel = onHoldDragCancel + }), + DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers< + DoubleFinerTapGestureRecognizer>( + () => DoubleFinerTapGestureRecognizer(), (instance) { + instance.onDoubleFinerTap = onDoubleFinerTap; + }), + CustomTouchGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => CustomTouchGestureRecognizer(), (instance) { + instance + ..onOneFingerPanStart = onOneFingerPanStart + ..onOneFingerPanUpdate = onOneFingerPanUpdate + ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate + ..onTwoFingerHorizontalDragUpdate = onTwoFingerHorizontalDragUpdate + ..onTwoFingerVerticalDragUpdate = onTwoFingerVerticalDragUpdate; + }) + }); +} diff --git a/lib/home_page.dart b/lib/home_page.dart index 93b3166d3..577812c98 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -24,7 +24,7 @@ class _HomePageState extends State { @override void initState() { super.initState(); - nowCtx = context; + currentCtx = context; if (isAndroid) { Timer(Duration(seconds: 5), () { _updateUrl = FFI.getByName('software_update_url'); diff --git a/lib/remote_page.dart b/lib/remote_page.dart index ede3a8105..397f89ac0 100644 --- a/lib/remote_page.dart +++ b/lib/remote_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; @@ -7,6 +8,7 @@ import 'dart:async'; import 'package:tuple/tuple.dart'; import 'package:wakelock/wakelock.dart'; import 'common.dart'; +import 'gestures.dart'; import 'model.dart'; final initText = '\1' * 1024; @@ -267,7 +269,7 @@ class _RemotePageState extends State { color: Colors.black, child: isDesktop ? getBodyForDesktopWithListener() - : SafeArea(child: getBodyForMobileWithGuesture())), + : SafeArea(child: getBodyForMobileWithGesture())), )), ); } @@ -346,7 +348,50 @@ class _RemotePageState extends State { ); } - Widget getBodyForMobileWithGuesture() { + Widget getBodyForMobileWithGesture() { + return getMixinGestureDetector( + child: getBodyForMobile(), + onTapUp: (d) { + if (_drag || _scroll) return; + if (_touchMode) { + FFI.cursorModel.touch( + d.localPosition.dx, d.localPosition.dy, _right); + } else { + FFI.tap(_right); + } + }, + onDoubleTap: () { + if (_drag || _scroll) return; + FFI.tap(_right); + FFI.tap(_right); + }, + onDoubleFinerTap: (d) { + if (_drag || _scroll) return; + FFI.tap(true); + }, + onHoldDragStart: (d) { + FFI.sendMouse('down', 'left'); + }, + onHoldDragUpdate: (d) { + FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, _touchMode, _drag); + }, + onHoldDragCancel: () { + FFI.sendMouse('up', 'left'); + }, + onOneFingerPanStart: (d) { + FFI.sendMouse('up', 'left'); + }, + onOneFingerPanUpdate: (d) { + FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, _touchMode, _drag); + }, + onTwoFingerScaleUpdate: (d) { + var scale = (d.scale -1) / 20 + 1; + FFI.canvasModel.updateScale(scale); + _scale = scale; + }, + onTwoFingerVerticalDragUpdate: (d) { + FFI.scroll( - d.delta.dy / 20); + }); return GestureDetector( onLongPress: () { if (_drag || _scroll) return; @@ -429,8 +474,8 @@ class _RemotePageState extends State { enableSuggestions: false, focusNode: _focusNode, maxLines: null, - initialValue: - _value, // trick way to make backspace work always + initialValue: _value, + // trick way to make backspace work always keyboardType: TextInputType.multiline, onChanged: handleInput, ), @@ -578,11 +623,10 @@ class _RemotePageState extends State { return TextButton( style: TextButton.styleFrom( minimumSize: Size(0, 0), - padding: EdgeInsets.symmetric( - vertical: 10, - horizontal: 9.75), //adds padding inside the button - tapTargetSize: MaterialTapTargetSize - .shrinkWrap, //limits the touch area to the button area + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), + //adds padding inside the button + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + //limits the touch area to the button area shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.0), ), diff --git a/lib/server_page.dart b/lib/server_page.dart index 39ffbfc25..9081b59ed 100644 --- a/lib/server_page.dart +++ b/lib/server_page.dart @@ -22,13 +22,13 @@ class ServerPage extends StatelessWidget { itemBuilder: (context) { return [ PopupMenuItem( - child: Text("修改服务ID"), + child: Text(translate("Change ID")), value: "changeID", enabled: false, ), PopupMenuItem( - child: Text("修改服务密码"), - value: "changeID", + child: Text("Set your own password"), + value: "changePW", enabled: false, ) ]; @@ -70,7 +70,7 @@ class _ServerInfoState extends State { // TODO set ID / PASSWORD var _serverId = TextEditingController(text: ""); var _serverPasswd = TextEditingController(text: ""); - static const _emptyIdShow = "正在获取ID..."; + var _emptyIdShow = translate("connecting_status"); @override void initState() { @@ -97,7 +97,7 @@ class _ServerInfoState extends State { controller: _serverId, decoration: InputDecoration( icon: const Icon(Icons.perm_identity), - labelText: '服务ID', + labelText: translate("ID"), labelStyle: TextStyle(fontWeight: FontWeight.bold, color: MyTheme.accent50), ), @@ -113,13 +113,12 @@ class _ServerInfoState extends State { controller: _serverPasswd, decoration: InputDecoration( icon: const Icon(Icons.lock), - labelText: '密码', + labelText: translate("Password"), labelStyle: TextStyle( fontWeight: FontWeight.bold, color: MyTheme.accent50), suffix: IconButton( icon: Icon(Icons.visibility), onPressed: () { - debugPrint("icon btn"); setState(() { _passwdShow = !_passwdShow; }); @@ -159,7 +158,7 @@ class _PermissionCheckerState extends State { @override void initState() { super.initState(); - nowCtx = context; + currentCtx = context; } @override @@ -169,20 +168,20 @@ class _PermissionCheckerState extends State { return myCard(Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - cardTitle("权限列表"), - PermissionRow("媒体权限", serverModel.mediaOk, _toAndroidInitService), + cardTitle(translate("Configuration Permissions")), + PermissionRow(translate("Media"), serverModel.mediaOk, _toAndroidInitService), const Divider(height: 0), - PermissionRow("输入权限", serverModel.inputOk, _toAndroidInitInput), + PermissionRow(translate("Input"), serverModel.inputOk, _toAndroidInitInput), const Divider(), serverModel.mediaOk ? ElevatedButton.icon( icon: Icon(Icons.stop), onPressed: _toAndroidStopService, - label: Text("Stop")) + label: Text(translate("Stop service"))) : ElevatedButton.icon( icon: Icon(Icons.play_arrow), onPressed: _toAndroidInitService, - label: Text("Start")), + label: Text(translate("Start Service"))), ], )); } @@ -191,17 +190,32 @@ class _PermissionCheckerState extends State { BuildContext loginReqAlertCtx; void showLoginReqAlert(BuildContext context, String peerID, String name) async { - debugPrint("got try_start_without_auth"); await showDialog( context: context, builder: (alertContext) { loginReqAlertCtx = alertContext; return AlertDialog( - title: Text("收到连接请求"), - content: Text("是否同意来自$name:$peerID的控制?"), + title: Text("Control Request"), + content:Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("Do you accept?")), + SizedBox(width: 20), + Row( + children: [ + CircleAvatar(child: Text(name[0])), + SizedBox(width: 10), + Text(name), + SizedBox(width: 5), + Text(peerID) + ], + ), + ], + ), actions: [ TextButton( - child: Text("接受"), + child: Text(translate("Accept")), onPressed: () { FFI.setByName("login_res", "true"); if (!FFI.serverModel.isFileTransfer) { @@ -211,7 +225,7 @@ void showLoginReqAlert(BuildContext context, String peerID, String name) async { Navigator.of(alertContext).pop(); }), TextButton( - child: Text("不接受"), + child: Text(translate("Dismiss")), onPressed: () { FFI.setByName("login_res", "false"); Navigator.of(alertContext).pop(); @@ -219,7 +233,6 @@ void showLoginReqAlert(BuildContext context, String peerID, String name) async { ], ); }); - debugPrint("alert done"); loginReqAlertCtx = null; } @@ -247,14 +260,14 @@ class PermissionRow extends StatelessWidget { text: name + ": ", style: TextStyle(fontSize: 16.0, color: MyTheme.accent50)), TextSpan( - text: isOk ? "已开启" : "未开启", + text: isOk ? translate("ON") : translate("OFF"), style: TextStyle( fontSize: 16.0, color: isOk ? Colors.green : Colors.grey)), ])), TextButton( onPressed: isOk ? null : onPressed, - child: const Text( - "去开启", + child: Text( + translate("OPEN"), style: TextStyle(fontWeight: FontWeight.bold), )), ], @@ -360,7 +373,7 @@ void toAndroidChannelInit() { debugPrint( "pre show loginAlert:${FFI.serverModel.isFileTransfer.toString()}"); showLoginReqAlert( - nowCtx, FFI.serverModel.peerID, FFI.serverModel.peerName); + currentCtx, FFI.serverModel.peerID, FFI.serverModel.peerName); debugPrint("from jvm:try_start_without_auth done"); break; }