Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions example/main.dart
Original file line number Diff line number Diff line change
@@ -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());
Expand All @@ -23,7 +23,11 @@ class HomePage extends StatelessWidget {
title: Text('Control Pad Example'),
),
body: Container(
child: JoystickView(),
child: VerticalJoystickView(
onDirectionChanged: (distance) {
print(distance);
},
),
),
);
}
Expand Down
1 change: 1 addition & 0 deletions lib/control_pad.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ library control_pad;

export 'views/joystick_view.dart';
export 'views/pad_button_view.dart';
export 'views/vertical_joystick_view.dart';
260 changes: 260 additions & 0 deletions lib/views/vertical_joystick_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
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: <Widget>[
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);
},
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,
);
},
),
);
}

List<Widget> createArrows() {
return [
Positioned(
child: Icon(
Icons.arrow_upward,
color: iconsColor,
),
top: 16.0,
left: 0.0,
right: 0.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);
}
}
73 changes: 73 additions & 0 deletions lib/views/vertical_view.dart
Original file line number Diff line number Diff line change
@@ -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> 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>[
BoxShadow(
color: Colors.black12,
spreadRadius: 8.0,
blurRadius: 8.0,
)
],
);
}
Loading