From 326b4288bdd0e5c32a84c0d0ce10e6f36548469b Mon Sep 17 00:00:00 2001 From: johntyty912 Date: Fri, 23 Oct 2020 11:17:04 +0800 Subject: [PATCH 1/4] implement vertical joystick --- example/main.dart | 8 +- lib/views/vertical_joystick_view.dart | 265 ++++++++++++++++++++++++++ lib/views/vertical_view.dart | 73 +++++++ pubspec.lock | 97 +++------- 4 files changed, 375 insertions(+), 68 deletions(-) create mode 100644 lib/views/vertical_joystick_view.dart create mode 100644 lib/views/vertical_view.dart diff --git a/example/main.dart b/example/main.dart index 4f2e095..9c06311 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,5 +1,5 @@ +import 'package:control_pad/views/vertical_joystick_view.dart'; import 'package:flutter/material.dart'; -import 'package:control_pad/control_pad.dart'; void main() { runApp(ExampleApp()); @@ -23,7 +23,11 @@ class HomePage extends StatelessWidget { title: Text('Control Pad Example'), ), body: Container( - child: JoystickView(), + child: VerticalJoystickView( + onDirectionChanged: (distance) { + print(distance); + }, + ), ), ); } diff --git a/lib/views/vertical_joystick_view.dart b/lib/views/vertical_joystick_view.dart new file mode 100644 index 0000000..69f3abc --- /dev/null +++ b/lib/views/vertical_joystick_view.dart @@ -0,0 +1,265 @@ +import 'dart:math' as _math; + +import 'package:control_pad/views/vertical_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'circle_view.dart'; + +typedef VerticalJoystickDirectionCallback = void Function(double distance); + +class VerticalJoystickView extends StatelessWidget { + /// The size of the joystick. + /// + /// Defaults to half of the width in the portrait + /// or half of the height in the landscape mode + final double size; + + /// Color of the icons + /// + /// Defaults to [Colors.white54] + final Color iconsColor; + + /// Color of the joystick background + /// + /// Defaults to [Colors.blueGrey] + final Color backgroundColor; + + /// Color of the inner (smaller) circle background + /// + /// Defaults to [Colors.blueGrey] + final Color innerCircleColor; + + /// Opacity of the joystick + /// + /// The opacity applies to the whole joystick including icons + /// + /// Defaults to [null] which means there will be no [Opacity] widget used + final double opacity; + + /// Callback to be called when user pans the joystick + /// + /// Defaults to [null] + final VerticalJoystickDirectionCallback onDirectionChanged; + + /// Indicates how often the [onDirectionChanged] should be called. + /// + /// Defaults to [null] which means there will be no lower limit. + /// Setting it to ie. 1 second will cause the callback to be not called more often + /// than once per second. + /// + /// The exception is the [onDirectionChanged] callback being called + /// on the [onPanStart] and [onPanEnd] callbacks. It will be called immediately. + final Duration interval; + + /// Shows top/right/bottom/left arrows on top of Joystick + /// + /// Defaults to [true] + final bool showArrows; + + VerticalJoystickView( + {this.size, + this.iconsColor = Colors.white54, + this.backgroundColor = Colors.blueGrey, + this.innerCircleColor = Colors.blueGrey, + this.opacity, + this.onDirectionChanged, + this.interval, + this.showArrows = true}); + + @override + Widget build(BuildContext context) { + double actualSize = size != null + ? size + : _math.min(MediaQuery.of(context).size.width, + MediaQuery.of(context).size.height) * + 0.5; + double innerCircleSize = actualSize / 2; + Offset lastPosition = Offset(innerCircleSize, innerCircleSize); + Offset joystickInnerPosition = _calculatePositionOfInnerCircle( + lastPosition, innerCircleSize, actualSize, Offset(0, 0)); + + DateTime _callbackTimestamp; + + return Center( + child: StatefulBuilder( + builder: (context, setState) { + Widget joystick = Stack( + children: [ + VerticalView.verticalJoystickCircle( + actualSize, + backgroundColor, + ), + Positioned( + child: CircleView.joystickInnerCircle( + actualSize / 2, + innerCircleColor, + ), + top: joystickInnerPosition.dy, +// left: joystickInnerPosition.dx, + left: 0.0, + ), + if (showArrows) ...createArrows(), + ], + ); + + return GestureDetector( + onPanUpdate: (details) { + _callbackTimestamp = _processGesture(actualSize, actualSize / 2, + details.localPosition, _callbackTimestamp); + joystickInnerPosition = _calculatePositionOfInnerCircle( + lastPosition, + innerCircleSize, + actualSize, + details.localPosition); + + setState(() => lastPosition = details.localPosition); + }, + child: (opacity != null) + ? Opacity(opacity: opacity, child: joystick) + : joystick, + ); + }, + ), + ); + } + + List createArrows() { + return [ + Positioned( + child: Icon( + Icons.arrow_upward, + color: iconsColor, + ), + top: 16.0, + left: 0.0, + right: 0.0, + ), + Positioned( + child: Icon( + Icons.arrow_back, + color: iconsColor, + ), + top: 0.0, + bottom: 0.0, + left: 16.0, + ), + Positioned( + child: Icon( + Icons.arrow_forward, + color: iconsColor, + ), + top: 0.0, + bottom: 0.0, + right: 16.0, + ), + Positioned( + child: Icon( + Icons.arrow_downward, + color: iconsColor, + ), + bottom: 16.0, + left: 0.0, + right: 0.0, + ), + ]; + } + + DateTime _processGesture(double size, double ignoreSize, Offset offset, + DateTime callbackTimestamp) { + double middle = size / 2.0; + + double distance = (middle - offset.dy); + + double normalizedDistance = + _math.max(_math.min(distance / (size / 2), 1.0), -1.0); + + DateTime _callbackTimestamp = callbackTimestamp; + if (onDirectionChanged != null && + _canCallOnDirectionChanged(callbackTimestamp)) { + _callbackTimestamp = DateTime.now(); + onDirectionChanged(normalizedDistance); + } + + return _callbackTimestamp; + } + + /// Checks if the [onDirectionChanged] can be called. + /// + /// Returns true if enough time has passed since last time it was called + /// or when there is no [interval] set. + bool _canCallOnDirectionChanged(DateTime callbackTimestamp) { + if (interval != null && callbackTimestamp != null) { + int intervalMilliseconds = interval.inMilliseconds; + int timestampMilliseconds = callbackTimestamp.millisecondsSinceEpoch; + int currentTimeMilliseconds = DateTime.now().millisecondsSinceEpoch; + + if (currentTimeMilliseconds - timestampMilliseconds <= + intervalMilliseconds) { + return false; + } + } + + return true; + } + + Offset _calculatePositionOfInnerCircle( + Offset lastPosition, double innerCircleSize, double size, Offset offset) { + double middle = size / 2.0; + + double angle = _math.atan2(offset.dy - middle, offset.dx - middle); + double degrees = angle * 180 / _math.pi; + if (offset.dx < middle && offset.dy < middle) { + degrees = 360 + degrees; + } + bool isStartPosition = lastPosition.dx == innerCircleSize && + lastPosition.dy == innerCircleSize; + double lastAngleRadians = + (isStartPosition) ? 0 : (degrees) * (_math.pi / 180.0); + + var rBig = size / 2; + var rSmall = innerCircleSize / 2; + + var x = (lastAngleRadians == -1) + ? rBig - rSmall + : (rBig - rSmall) + (rBig - rSmall) * _math.cos(lastAngleRadians); + var y = (lastAngleRadians == -1) + ? rBig - rSmall + : (rBig - rSmall) + (rBig - rSmall) * _math.sin(lastAngleRadians); + + var xPosition = lastPosition.dx - rSmall; + var yPosition = lastPosition.dy - rSmall; + + var angleRadianPlus = lastAngleRadians + _math.pi / 2; + if (angleRadianPlus < _math.pi / 2) { + if (xPosition > x) { + xPosition = x; + } + if (yPosition < y) { + yPosition = y; + } + } else if (angleRadianPlus < _math.pi) { + if (xPosition > x) { + xPosition = x; + } + if (yPosition > y) { + yPosition = y; + } + } else if (angleRadianPlus < 3 * _math.pi / 2) { + if (xPosition < x) { + xPosition = x; + } + if (yPosition > y) { + yPosition = y; + } + } else { + if (xPosition < x) { + xPosition = x; + } + if (yPosition < y) { + yPosition = y; + } + } + return Offset(xPosition, yPosition); + } +} diff --git a/lib/views/vertical_view.dart b/lib/views/vertical_view.dart new file mode 100644 index 0000000..feb27ce --- /dev/null +++ b/lib/views/vertical_view.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class VerticalView extends StatelessWidget { + final double size; + + final Color color; + + final List boxShadow; + + final Border border; + + final double opacity; + + final Image buttonImage; + + final Icon buttonIcon; + + final String buttonText; + + VerticalView({ + this.size, + this.color = Colors.transparent, + this.boxShadow, + this.border, + this.opacity, + this.buttonImage, + this.buttonIcon, + this.buttonText, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size / 2, + height: size, + child: Center( + child: buttonIcon != null + ? buttonIcon + : (buttonImage != null) + ? buttonImage + : (buttonText != null) + ? Text(buttonText) + : null, + ), + decoration: BoxDecoration( + color: color, + shape: BoxShape.rectangle, + border: border, + boxShadow: boxShadow, + borderRadius: new BorderRadius.circular((size / 2)), + ), + ); + } + + factory VerticalView.verticalJoystickCircle(double size, Color color) => + VerticalView( + size: size, + color: color, + border: Border.all( + color: Colors.black45, + width: 4.0, + style: BorderStyle.solid, + ), + boxShadow: [ + BoxShadow( + color: Colors.black12, + spreadRadius: 8.0, + blurRadius: 8.0, + ) + ], + ); +} diff --git a/pubspec.lock b/pubspec.lock index ea53bf7..66db214 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,62 +1,55 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.5.0-nullsafety.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.1.0-nullsafety.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.3" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" - collection: + version: "1.2.0-nullsafety.1" + clock: dependency: transitive description: - name: collection + name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" - convert: + version: "1.1.0-nullsafety.1" + collection: dependency: transitive description: - name: convert + name: collection url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" - crypto: + version: "1.15.0-nullsafety.3" + fake_async: dependency: transitive description: - name: crypto + name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "1.2.0-nullsafety.1" flutter: dependency: "direct main" description: flutter @@ -67,34 +60,27 @@ packages: description: flutter source: sdk version: "0.0.0" - image: - dependency: transitive - description: - name: image - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.4" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.10-nullsafety.1" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0-nullsafety.3" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.8.0-nullsafety.1" pedantic: dependency: "direct dev" description: @@ -102,20 +88,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.0" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" sky_engine: dependency: transitive description: flutter @@ -127,62 +99,55 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.8.0-nullsafety.2" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0-nullsafety.1" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety.1" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0-nullsafety.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.19-nullsafety.2" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0-nullsafety.3" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "3.5.0" + version: "2.1.0-nullsafety.3" sdks: - dart: ">=2.4.0 <3.0.0" + dart: ">=2.10.0-110 <2.11.0" From 2ef15955c0c0847de9dc7a70942febf64a18fbde Mon Sep 17 00:00:00 2001 From: johntyty912 Date: Fri, 23 Oct 2020 16:28:56 +0800 Subject: [PATCH 2/4] export vertical joystick view --- lib/control_pad.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/control_pad.dart b/lib/control_pad.dart index ed2411a..b228fc9 100644 --- a/lib/control_pad.dart +++ b/lib/control_pad.dart @@ -2,3 +2,4 @@ library control_pad; export 'views/joystick_view.dart'; export 'views/pad_button_view.dart'; +export 'views/vertical_joystick_view.dart'; From ba48a3d9f9ede5185d4f75c2e46ce6b83dfbbc74 Mon Sep 17 00:00:00 2001 From: johntyty912 Date: Fri, 23 Oct 2020 16:39:25 +0800 Subject: [PATCH 3/4] delete the useless left arrow and right arrow --- lib/views/vertical_joystick_view.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/views/vertical_joystick_view.dart b/lib/views/vertical_joystick_view.dart index 69f3abc..990d6f4 100644 --- a/lib/views/vertical_joystick_view.dart +++ b/lib/views/vertical_joystick_view.dart @@ -135,24 +135,6 @@ class VerticalJoystickView extends StatelessWidget { left: 0.0, right: 0.0, ), - Positioned( - child: Icon( - Icons.arrow_back, - color: iconsColor, - ), - top: 0.0, - bottom: 0.0, - left: 16.0, - ), - Positioned( - child: Icon( - Icons.arrow_forward, - color: iconsColor, - ), - top: 0.0, - bottom: 0.0, - right: 16.0, - ), Positioned( child: Icon( Icons.arrow_downward, From 36c3549b50d943d2f9b26b5e4e280d058e0a3e9c Mon Sep 17 00:00:00 2001 From: johntyty912 Date: Mon, 14 Dec 2020 10:40:09 +0800 Subject: [PATCH 4/4] make vertical joystick return original position onPanEnd --- lib/views/vertical_joystick_view.dart | 13 +++++++++ pubspec.lock | 38 +++++++++++++-------------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/views/vertical_joystick_view.dart b/lib/views/vertical_joystick_view.dart index 990d6f4..5ed3207 100644 --- a/lib/views/vertical_joystick_view.dart +++ b/lib/views/vertical_joystick_view.dart @@ -115,6 +115,19 @@ class VerticalJoystickView extends StatelessWidget { setState(() => lastPosition = details.localPosition); }, + onPanEnd: (details) { + _callbackTimestamp = null; + if (onDirectionChanged != null) { + onDirectionChanged(0); + } + joystickInnerPosition = _calculatePositionOfInnerCircle( + Offset(innerCircleSize, innerCircleSize), + innerCircleSize, + actualSize, + Offset(0, 0)); + setState(() => + lastPosition = Offset(innerCircleSize, innerCircleSize)); + }, child: (opacity != null) ? Opacity(opacity: opacity, child: joystick) : joystick, diff --git a/pubspec.lock b/pubspec.lock index 66db214..c254632 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,49 +7,49 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.5.0-nullsafety.3" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0-nullsafety.3" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0-nullsafety.5" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0-nullsafety.3" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0-nullsafety.3" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.15.0-nullsafety.5" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0-nullsafety.3" flutter: dependency: "direct main" description: flutter @@ -66,21 +66,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.10-nullsafety.3" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0-nullsafety.6" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" + version: "1.8.0-nullsafety.3" pedantic: dependency: "direct dev" description: @@ -99,55 +99,55 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0-nullsafety.4" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0-nullsafety.6" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0-nullsafety.3" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0-nullsafety.3" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0-nullsafety.3" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.19-nullsafety.6" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0-nullsafety.5" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0-nullsafety.5" sdks: - dart: ">=2.10.0-110 <2.11.0" + dart: ">=2.12.0-0.0 <3.0.0"