add custom gestures

This commit is contained in:
csf 2022-02-16 23:08:23 +08:00
parent b60e276c98
commit f5f496f1cf
4 changed files with 860 additions and 33 deletions

770
lib/gestures.dart Normal file
View File

@ -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<PointerDeviceKind> 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<PointerDeviceKind> 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<int, _TapTracker> _trackers = <int, _TapTracker>{};
@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<void>(
'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<void>('onHoldDragCancel', onHoldDragCancel);
}
}
@override
String get debugDescription => 'double tap';
}
class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
DoubleFinerTapGestureRecognizer({
Object debugOwner,
Set<PointerDeviceKind> supportedDevices,
}) : super(
debugOwner: debugOwner,
supportedDevices: supportedDevices,
);
GestureTapDownCallback onDoubleFinerTapDown;
GestureTapDownCallback onDoubleFinerTap;
GestureTapCancelCallback onDoubleFinerTapCancel;
Timer _firstTapTimer; // reject
_TapTracker _firstTap;
var _isStart = false;
final Set<int> _upTap = {};
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@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<void>(
'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<void>('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: <Type, GestureRecognizerFactory>{
// Official
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(), (instance) {
instance.onTapUp = onTapUp;
}),
DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(), (instance) {
instance.onDoubleTap = onDoubleTap;
}),
// Customized
HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
() => HoldTapMoveGestureRecognizer(),
(instance) => {
instance
..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate
..onHoldDragCancel = onHoldDragCancel
}),
DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
DoubleFinerTapGestureRecognizer>(
() => DoubleFinerTapGestureRecognizer(), (instance) {
instance.onDoubleFinerTap = onDoubleFinerTap;
}),
CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
() => CustomTouchGestureRecognizer(), (instance) {
instance
..onOneFingerPanStart = onOneFingerPanStart
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerHorizontalDragUpdate = onTwoFingerHorizontalDragUpdate
..onTwoFingerVerticalDragUpdate = onTwoFingerVerticalDragUpdate;
})
});
}

View File

@ -24,7 +24,7 @@ class _HomePageState extends State<HomePage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
nowCtx = context; currentCtx = context;
if (isAndroid) { if (isAndroid) {
Timer(Duration(seconds: 5), () { Timer(Duration(seconds: 5), () {
_updateUrl = FFI.getByName('software_update_url'); _updateUrl = FFI.getByName('software_update_url');

View File

@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -7,6 +8,7 @@ import 'dart:async';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
import 'common.dart'; import 'common.dart';
import 'gestures.dart';
import 'model.dart'; import 'model.dart';
final initText = '\1' * 1024; final initText = '\1' * 1024;
@ -267,7 +269,7 @@ class _RemotePageState extends State<RemotePage> {
color: Colors.black, color: Colors.black,
child: isDesktop child: isDesktop
? getBodyForDesktopWithListener() ? getBodyForDesktopWithListener()
: SafeArea(child: getBodyForMobileWithGuesture())), : SafeArea(child: getBodyForMobileWithGesture())),
)), )),
); );
} }
@ -346,7 +348,50 @@ class _RemotePageState extends State<RemotePage> {
); );
} }
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( return GestureDetector(
onLongPress: () { onLongPress: () {
if (_drag || _scroll) return; if (_drag || _scroll) return;
@ -429,8 +474,8 @@ class _RemotePageState extends State<RemotePage> {
enableSuggestions: false, enableSuggestions: false,
focusNode: _focusNode, focusNode: _focusNode,
maxLines: null, maxLines: null,
initialValue: initialValue: _value,
_value, // trick way to make backspace work always // trick way to make backspace work always
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
onChanged: handleInput, onChanged: handleInput,
), ),
@ -578,11 +623,10 @@ class _RemotePageState extends State<RemotePage> {
return TextButton( return TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
minimumSize: Size(0, 0), minimumSize: Size(0, 0),
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75),
vertical: 10, //adds padding inside the button
horizontal: 9.75), //adds padding inside the button tapTargetSize: MaterialTapTargetSize.shrinkWrap,
tapTargetSize: MaterialTapTargetSize //limits the touch area to the button area
.shrinkWrap, //limits the touch area to the button area
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0), borderRadius: BorderRadius.circular(5.0),
), ),

View File

@ -22,13 +22,13 @@ class ServerPage extends StatelessWidget {
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
PopupMenuItem( PopupMenuItem(
child: Text("修改服务ID"), child: Text(translate("Change ID")),
value: "changeID", value: "changeID",
enabled: false, enabled: false,
), ),
PopupMenuItem( PopupMenuItem(
child: Text("修改服务密码"), child: Text("Set your own password"),
value: "changeID", value: "changePW",
enabled: false, enabled: false,
) )
]; ];
@ -70,7 +70,7 @@ class _ServerInfoState extends State<ServerInfo> {
// TODO set ID / PASSWORD // TODO set ID / PASSWORD
var _serverId = TextEditingController(text: ""); var _serverId = TextEditingController(text: "");
var _serverPasswd = TextEditingController(text: ""); var _serverPasswd = TextEditingController(text: "");
static const _emptyIdShow = "正在获取ID..."; var _emptyIdShow = translate("connecting_status");
@override @override
void initState() { void initState() {
@ -97,7 +97,7 @@ class _ServerInfoState extends State<ServerInfo> {
controller: _serverId, controller: _serverId,
decoration: InputDecoration( decoration: InputDecoration(
icon: const Icon(Icons.perm_identity), icon: const Icon(Icons.perm_identity),
labelText: '服务ID', labelText: translate("ID"),
labelStyle: labelStyle:
TextStyle(fontWeight: FontWeight.bold, color: MyTheme.accent50), TextStyle(fontWeight: FontWeight.bold, color: MyTheme.accent50),
), ),
@ -113,13 +113,12 @@ class _ServerInfoState extends State<ServerInfo> {
controller: _serverPasswd, controller: _serverPasswd,
decoration: InputDecoration( decoration: InputDecoration(
icon: const Icon(Icons.lock), icon: const Icon(Icons.lock),
labelText: '密码', labelText: translate("Password"),
labelStyle: TextStyle( labelStyle: TextStyle(
fontWeight: FontWeight.bold, color: MyTheme.accent50), fontWeight: FontWeight.bold, color: MyTheme.accent50),
suffix: IconButton( suffix: IconButton(
icon: Icon(Icons.visibility), icon: Icon(Icons.visibility),
onPressed: () { onPressed: () {
debugPrint("icon btn");
setState(() { setState(() {
_passwdShow = !_passwdShow; _passwdShow = !_passwdShow;
}); });
@ -159,7 +158,7 @@ class _PermissionCheckerState extends State<PermissionChecker> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
nowCtx = context; currentCtx = context;
} }
@override @override
@ -169,20 +168,20 @@ class _PermissionCheckerState extends State<PermissionChecker> {
return myCard(Column( return myCard(Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
cardTitle("权限列表"), cardTitle(translate("Configuration Permissions")),
PermissionRow("媒体权限", serverModel.mediaOk, _toAndroidInitService), PermissionRow(translate("Media"), serverModel.mediaOk, _toAndroidInitService),
const Divider(height: 0), const Divider(height: 0),
PermissionRow("输入权限", serverModel.inputOk, _toAndroidInitInput), PermissionRow(translate("Input"), serverModel.inputOk, _toAndroidInitInput),
const Divider(), const Divider(),
serverModel.mediaOk serverModel.mediaOk
? ElevatedButton.icon( ? ElevatedButton.icon(
icon: Icon(Icons.stop), icon: Icon(Icons.stop),
onPressed: _toAndroidStopService, onPressed: _toAndroidStopService,
label: Text("Stop")) label: Text(translate("Stop service")))
: ElevatedButton.icon( : ElevatedButton.icon(
icon: Icon(Icons.play_arrow), icon: Icon(Icons.play_arrow),
onPressed: _toAndroidInitService, onPressed: _toAndroidInitService,
label: Text("Start")), label: Text(translate("Start Service"))),
], ],
)); ));
} }
@ -191,17 +190,32 @@ class _PermissionCheckerState extends State<PermissionChecker> {
BuildContext loginReqAlertCtx; BuildContext loginReqAlertCtx;
void showLoginReqAlert(BuildContext context, String peerID, String name) async { void showLoginReqAlert(BuildContext context, String peerID, String name) async {
debugPrint("got try_start_without_auth");
await showDialog( await showDialog(
context: context, context: context,
builder: (alertContext) { builder: (alertContext) {
loginReqAlertCtx = alertContext; loginReqAlertCtx = alertContext;
return AlertDialog( return AlertDialog(
title: Text("收到连接请求"), title: Text("Control Request"),
content: Text("是否同意来自$name:$peerID的控制"), 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: [ actions: [
TextButton( TextButton(
child: Text("接受"), child: Text(translate("Accept")),
onPressed: () { onPressed: () {
FFI.setByName("login_res", "true"); FFI.setByName("login_res", "true");
if (!FFI.serverModel.isFileTransfer) { if (!FFI.serverModel.isFileTransfer) {
@ -211,7 +225,7 @@ void showLoginReqAlert(BuildContext context, String peerID, String name) async {
Navigator.of(alertContext).pop(); Navigator.of(alertContext).pop();
}), }),
TextButton( TextButton(
child: Text("不接受"), child: Text(translate("Dismiss")),
onPressed: () { onPressed: () {
FFI.setByName("login_res", "false"); FFI.setByName("login_res", "false");
Navigator.of(alertContext).pop(); Navigator.of(alertContext).pop();
@ -219,7 +233,6 @@ void showLoginReqAlert(BuildContext context, String peerID, String name) async {
], ],
); );
}); });
debugPrint("alert done");
loginReqAlertCtx = null; loginReqAlertCtx = null;
} }
@ -247,14 +260,14 @@ class PermissionRow extends StatelessWidget {
text: name + ": ", text: name + ": ",
style: TextStyle(fontSize: 16.0, color: MyTheme.accent50)), style: TextStyle(fontSize: 16.0, color: MyTheme.accent50)),
TextSpan( TextSpan(
text: isOk ? "已开启" : "未开启", text: isOk ? translate("ON") : translate("OFF"),
style: TextStyle( style: TextStyle(
fontSize: 16.0, color: isOk ? Colors.green : Colors.grey)), fontSize: 16.0, color: isOk ? Colors.green : Colors.grey)),
])), ])),
TextButton( TextButton(
onPressed: isOk ? null : onPressed, onPressed: isOk ? null : onPressed,
child: const Text( child: Text(
"去开启", translate("OPEN"),
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
)), )),
], ],
@ -360,7 +373,7 @@ void toAndroidChannelInit() {
debugPrint( debugPrint(
"pre show loginAlert:${FFI.serverModel.isFileTransfer.toString()}"); "pre show loginAlert:${FFI.serverModel.isFileTransfer.toString()}");
showLoginReqAlert( showLoginReqAlert(
nowCtx, FFI.serverModel.peerID, FFI.serverModel.peerName); currentCtx, FFI.serverModel.peerID, FFI.serverModel.peerName);
debugPrint("from jvm:try_start_without_auth done"); debugPrint("from jvm:try_start_without_auth done");
break; break;
} }