WIP #212 Light card support
This commit is contained in:
parent
38c37fa212
commit
127e0b8182
@ -62,6 +62,10 @@ class CardWidget extends StatelessWidget {
|
||||
return _buildGaugeCard(context);
|
||||
}
|
||||
|
||||
case CardType.LIGHT: {
|
||||
return _buildLightCard(context);
|
||||
}
|
||||
|
||||
case CardType.MARKDOWN: {
|
||||
return _buildMarkdownCard(context);
|
||||
}
|
||||
@ -306,6 +310,22 @@ class CardWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLightCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: LightCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(name: card.name ?? ""));
|
||||
|
90
lib/cards/widgets/light_card_body.dart
Normal file
90
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,90 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LightCardBodyState createState() => _LightCardBodyState();
|
||||
}
|
||||
|
||||
class _LightCardBodyState extends State<LightCardBody> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
Logger.d("Light brightness: ${entity.brightness}");
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Container(
|
||||
//color: Colors.redAccent,
|
||||
child: SingleCircularSlider(
|
||||
255,
|
||||
entity.brightness ?? 0,
|
||||
baseColor: Colors.white,
|
||||
handlerColor: Colors.blue[200],
|
||||
selectionColor: Colors.blue[100],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -96,6 +96,7 @@ class CardType {
|
||||
static const CONDITIONAL = "conditional";
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
|
@ -22,7 +22,9 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||
|
||||
import 'utils/logger.dart';
|
||||
|
||||
part 'const.dart';
|
||||
part 'utils/launcher.dart';
|
||||
@ -115,10 +117,10 @@ part 'cards/card_widget.dart';
|
||||
part 'cards/widgets/card_header.widget.dart';
|
||||
part 'panels/config_panel_widget.dart';
|
||||
part 'panels/widgets/link_to_web_config.dart';
|
||||
part 'utils/logger.dart';
|
||||
part 'types/ha_error.dart';
|
||||
part 'types/event_bus_events.dart';
|
||||
part 'cards/widgets/gauge_card_body.dart';
|
||||
part 'cards/widgets/light_card_body.dart';
|
||||
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
|
77
lib/plugins/circular_slider/base_painter.dart
Normal file
77
lib/plugins/circular_slider/base_painter.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
class BasePainter extends CustomPainter {
|
||||
Color baseColor;
|
||||
Color selectionColor;
|
||||
int primarySectors;
|
||||
int secondarySectors;
|
||||
double sliderStrokeWidth;
|
||||
|
||||
Offset center;
|
||||
double radius;
|
||||
|
||||
BasePainter({
|
||||
@required this.baseColor,
|
||||
@required this.selectionColor,
|
||||
@required this.primarySectors,
|
||||
@required this.secondarySectors,
|
||||
@required this.sliderStrokeWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint base = _getPaint(color: baseColor);
|
||||
|
||||
center = Offset(size.width / 2, size.height / 2);
|
||||
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
|
||||
// we need this in the parent to calculate if the user clicks on the circumference
|
||||
|
||||
assert(radius > 0);
|
||||
|
||||
canvas.drawCircle(center, radius, base);
|
||||
|
||||
if (primarySectors > 0) {
|
||||
_paintSectors(primarySectors, 8.0, selectionColor, canvas);
|
||||
}
|
||||
|
||||
if (secondarySectors > 0) {
|
||||
_paintSectors(secondarySectors, 6.0, baseColor, canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void _paintSectors(
|
||||
int sectors, double radiusPadding, Color color, Canvas canvas) {
|
||||
Paint section = _getPaint(color: color, width: 2.0);
|
||||
|
||||
var endSectors =
|
||||
getSectionsCoordinatesInCircle(center, radius + radiusPadding, sectors);
|
||||
var initSectors =
|
||||
getSectionsCoordinatesInCircle(center, radius - radiusPadding, sectors);
|
||||
_paintLines(canvas, initSectors, endSectors, section);
|
||||
}
|
||||
|
||||
void _paintLines(
|
||||
Canvas canvas, List<Offset> inits, List<Offset> ends, Paint section) {
|
||||
assert(inits.length == ends.length && inits.length > 0);
|
||||
|
||||
for (var i = 0; i < inits.length; i++) {
|
||||
canvas.drawLine(inits[i], ends[i], section);
|
||||
}
|
||||
}
|
||||
|
||||
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
|
||||
Paint()
|
||||
..color = color
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = style ?? PaintingStyle.stroke
|
||||
..strokeWidth = width ?? sliderStrokeWidth;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
366
lib/plugins/circular_slider/circular_slider_paint.dart
Normal file
366
lib/plugins/circular_slider/circular_slider_paint.dart
Normal file
@ -0,0 +1,366 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base_painter.dart';
|
||||
import 'slider_painter.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
enum CircularSliderMode { singleHandler, doubleHandler }
|
||||
|
||||
enum SlidingState { none, endIsBiggerThanStart, endIsSmallerThanStart }
|
||||
|
||||
typedef SelectionChanged<T> = void Function(T a, T b, T c);
|
||||
|
||||
class CircularSliderPaint extends StatefulWidget {
|
||||
final CircularSliderMode mode;
|
||||
final int init;
|
||||
final int end;
|
||||
final int divisions;
|
||||
final int primarySectors;
|
||||
final int secondarySectors;
|
||||
final SelectionChanged<int> onSelectionChange;
|
||||
final SelectionChanged<int> onSelectionEnd;
|
||||
final Color baseColor;
|
||||
final Color selectionColor;
|
||||
final Color handlerColor;
|
||||
final double handlerOutterRadius;
|
||||
final Widget child;
|
||||
final bool showRoundedCapInSelection;
|
||||
final bool showHandlerOutter;
|
||||
final double sliderStrokeWidth;
|
||||
final bool shouldCountLaps;
|
||||
|
||||
CircularSliderPaint({
|
||||
@required this.mode,
|
||||
@required this.divisions,
|
||||
@required this.init,
|
||||
@required this.end,
|
||||
this.child,
|
||||
@required this.primarySectors,
|
||||
@required this.secondarySectors,
|
||||
@required this.onSelectionChange,
|
||||
@required this.onSelectionEnd,
|
||||
@required this.baseColor,
|
||||
@required this.selectionColor,
|
||||
@required this.handlerColor,
|
||||
@required this.handlerOutterRadius,
|
||||
@required this.showRoundedCapInSelection,
|
||||
@required this.showHandlerOutter,
|
||||
@required this.sliderStrokeWidth,
|
||||
@required this.shouldCountLaps,
|
||||
});
|
||||
|
||||
@override
|
||||
_CircularSliderState createState() => _CircularSliderState();
|
||||
}
|
||||
|
||||
class _CircularSliderState extends State<CircularSliderPaint> {
|
||||
bool _isInitHandlerSelected = false;
|
||||
bool _isEndHandlerSelected = false;
|
||||
|
||||
SliderPainter _painter;
|
||||
|
||||
/// start angle in radians where we need to locate the init handler
|
||||
double _startAngle;
|
||||
|
||||
/// end angle in radians where we need to locate the end handler
|
||||
double _endAngle;
|
||||
|
||||
/// the absolute angle in radians representing the selection
|
||||
double _sweepAngle;
|
||||
|
||||
/// in case we have a double slider and we want to move the whole selection by clicking in the slider
|
||||
/// this will capture the position in the selection relative to the initial handler
|
||||
/// that way we will be able to keep the selection constant when moving
|
||||
int _differenceFromInitPoint;
|
||||
|
||||
/// will store the number of full laps (2pi radians) as part of the selection
|
||||
int _laps = 0;
|
||||
|
||||
/// will be used to calculate in the next movement if we need to increase or decrease _laps
|
||||
SlidingState _slidingState = SlidingState.none;
|
||||
|
||||
bool get isDoubleHandler => widget.mode == CircularSliderMode.doubleHandler;
|
||||
|
||||
bool get isSingleHandler => widget.mode == CircularSliderMode.singleHandler;
|
||||
|
||||
bool get isBothHandlersSelected =>
|
||||
_isEndHandlerSelected && _isInitHandlerSelected;
|
||||
|
||||
bool get isNoHandlersSelected =>
|
||||
!_isEndHandlerSelected && !_isInitHandlerSelected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_calculatePaintData();
|
||||
}
|
||||
|
||||
// we need to update this widget both with gesture detector but
|
||||
// also when the parent widget rebuilds itself
|
||||
@override
|
||||
void didUpdateWidget(CircularSliderPaint oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
|
||||
_calculatePaintData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
CustomPanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
|
||||
() => CustomPanGestureRecognizer(
|
||||
onPanDown: _onPanDown,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
),
|
||||
(CustomPanGestureRecognizer instance) {},
|
||||
),
|
||||
},
|
||||
child: CustomPaint(
|
||||
painter: BasePainter(
|
||||
baseColor: widget.baseColor,
|
||||
selectionColor: widget.selectionColor,
|
||||
primarySectors: widget.primarySectors,
|
||||
secondarySectors: widget.secondarySectors,
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth,
|
||||
),
|
||||
foregroundPainter: _painter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _calculatePaintData() {
|
||||
var initPercent = isDoubleHandler
|
||||
? valueToPercentage(widget.init, widget.divisions)
|
||||
: 0.0;
|
||||
var endPercent = valueToPercentage(widget.end, widget.divisions);
|
||||
var sweep = getSweepAngle(initPercent, endPercent);
|
||||
|
||||
var previousStartAngle = _startAngle;
|
||||
var previousEndAngle = _endAngle;
|
||||
|
||||
_startAngle = isDoubleHandler ? percentageToRadians(initPercent) : 0.0;
|
||||
_endAngle = percentageToRadians(endPercent);
|
||||
_sweepAngle = percentageToRadians(sweep.abs());
|
||||
|
||||
// update full laps if need be
|
||||
if (widget.shouldCountLaps) {
|
||||
var newSlidingState = _calculateSlidingState(_startAngle, _endAngle);
|
||||
if (isSingleHandler) {
|
||||
_laps = _calculateLapsForsSingleHandler(
|
||||
_endAngle, previousEndAngle, _slidingState, _laps);
|
||||
_slidingState = newSlidingState;
|
||||
} else {
|
||||
// is double handler
|
||||
if (newSlidingState != _slidingState) {
|
||||
_laps = _calculateLapsForDoubleHandler(
|
||||
_startAngle,
|
||||
_endAngle,
|
||||
previousStartAngle,
|
||||
previousEndAngle,
|
||||
_slidingState,
|
||||
newSlidingState,
|
||||
_laps);
|
||||
_slidingState = newSlidingState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_painter = SliderPainter(
|
||||
mode: widget.mode,
|
||||
startAngle: _startAngle,
|
||||
endAngle: _endAngle,
|
||||
sweepAngle: _sweepAngle,
|
||||
selectionColor: widget.selectionColor,
|
||||
handlerColor: widget.handlerColor,
|
||||
handlerOutterRadius: widget.handlerOutterRadius,
|
||||
showRoundedCapInSelection: widget.showRoundedCapInSelection,
|
||||
showHandlerOutter: widget.showHandlerOutter,
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateLapsForsSingleHandler(
|
||||
double end, double prevEnd, SlidingState slidingState, int laps) {
|
||||
if (slidingState != SlidingState.none) {
|
||||
if (radiansWasModuloed(end, prevEnd)) {
|
||||
var lapIncrement = end < prevEnd ? 1 : -1;
|
||||
var newLaps = laps + lapIncrement;
|
||||
return newLaps < 0 ? 0 : newLaps;
|
||||
}
|
||||
}
|
||||
return laps;
|
||||
}
|
||||
|
||||
int _calculateLapsForDoubleHandler(
|
||||
double start,
|
||||
double end,
|
||||
double prevStart,
|
||||
double prevEnd,
|
||||
SlidingState slidingState,
|
||||
SlidingState newSlidingState,
|
||||
int laps) {
|
||||
if (slidingState != SlidingState.none) {
|
||||
if (!radiansWasModuloed(start, prevStart) &&
|
||||
!radiansWasModuloed(end, prevEnd)) {
|
||||
var lapIncrement =
|
||||
newSlidingState == SlidingState.endIsBiggerThanStart ? 1 : -1;
|
||||
var newLaps = laps + lapIncrement;
|
||||
return newLaps < 0 ? 0 : newLaps;
|
||||
}
|
||||
}
|
||||
return laps;
|
||||
}
|
||||
|
||||
SlidingState _calculateSlidingState(double start, double end) {
|
||||
return end > start
|
||||
? SlidingState.endIsBiggerThanStart
|
||||
: SlidingState.endIsSmallerThanStart;
|
||||
}
|
||||
|
||||
void _onPanUpdate(Offset details) {
|
||||
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
|
||||
return;
|
||||
}
|
||||
if (_painter.center == null) {
|
||||
return;
|
||||
}
|
||||
_handlePan(details, false);
|
||||
}
|
||||
|
||||
void _onPanEnd(Offset details) {
|
||||
_handlePan(details, true);
|
||||
|
||||
_isInitHandlerSelected = false;
|
||||
_isEndHandlerSelected = false;
|
||||
}
|
||||
|
||||
void _handlePan(Offset details, bool isPanEnd) {
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
var position = renderBox.globalToLocal(details);
|
||||
|
||||
var angle = coordinatesToRadians(_painter.center, position);
|
||||
var percentage = radiansToPercentage(angle);
|
||||
var newValue = percentageToValue(percentage, widget.divisions);
|
||||
|
||||
if (isBothHandlersSelected) {
|
||||
var newValueInit =
|
||||
(newValue - _differenceFromInitPoint) % widget.divisions;
|
||||
if (newValueInit != widget.init) {
|
||||
var newValueEnd =
|
||||
(widget.end + (newValueInit - widget.init)) % widget.divisions;
|
||||
widget.onSelectionChange(newValueInit, newValueEnd, _laps);
|
||||
if (isPanEnd) {
|
||||
widget.onSelectionEnd(newValueInit, newValueEnd, _laps);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// isDoubleHandler but one handler was selected
|
||||
if (_isInitHandlerSelected) {
|
||||
widget.onSelectionChange(newValue, widget.end, _laps);
|
||||
if (isPanEnd) {
|
||||
widget.onSelectionEnd(newValue, widget.end, _laps);
|
||||
}
|
||||
} else {
|
||||
widget.onSelectionChange(widget.init, newValue, _laps);
|
||||
if (isPanEnd) {
|
||||
widget.onSelectionEnd(widget.init, newValue, _laps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _onPanDown(Offset details) {
|
||||
if (_painter == null) {
|
||||
return false;
|
||||
}
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
var position = renderBox.globalToLocal(details);
|
||||
|
||||
if (position == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSingleHandler) {
|
||||
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
|
||||
_isEndHandlerSelected = true;
|
||||
_onPanUpdate(details);
|
||||
}
|
||||
} else {
|
||||
_isInitHandlerSelected = isPointInsideCircle(
|
||||
position, _painter.initHandler, widget.handlerOutterRadius);
|
||||
|
||||
if (!_isInitHandlerSelected) {
|
||||
_isEndHandlerSelected = isPointInsideCircle(
|
||||
position, _painter.endHandler, widget.handlerOutterRadius);
|
||||
}
|
||||
|
||||
if (isNoHandlersSelected) {
|
||||
// we check if the user pressed in the selection in a double handler slider
|
||||
// that means the user wants to move the selection as a whole
|
||||
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
|
||||
var angle = coordinatesToRadians(_painter.center, position);
|
||||
if (isAngleInsideRadiansSelection(angle, _startAngle, _sweepAngle)) {
|
||||
_isEndHandlerSelected = true;
|
||||
_isInitHandlerSelected = true;
|
||||
var positionPercentage = radiansToPercentage(angle);
|
||||
|
||||
// no need to account for negative values, that will be sorted out in the onPanUpdate
|
||||
_differenceFromInitPoint =
|
||||
percentageToValue(positionPercentage, widget.divisions) -
|
||||
widget.init;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _isInitHandlerSelected || _isEndHandlerSelected;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
final Function onPanDown;
|
||||
final Function onPanUpdate;
|
||||
final Function onPanEnd;
|
||||
|
||||
CustomPanGestureRecognizer({
|
||||
@required this.onPanDown,
|
||||
@required this.onPanUpdate,
|
||||
@required this.onPanEnd,
|
||||
});
|
||||
|
||||
@override
|
||||
void addPointer(PointerEvent event) {
|
||||
if (onPanDown(event.position)) {
|
||||
startTrackingPointer(event.pointer);
|
||||
resolve(GestureDisposition.accepted);
|
||||
} else {
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event) {
|
||||
if (event is PointerMoveEvent) {
|
||||
onPanUpdate(event.position);
|
||||
}
|
||||
if (event is PointerUpEvent) {
|
||||
onPanEnd(event.position);
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'customPan';
|
||||
|
||||
@override
|
||||
void didStopTrackingLastPointer(int pointer) {}
|
||||
}
|
148
lib/plugins/circular_slider/double_circular_slider.dart
Normal file
148
lib/plugins/circular_slider/double_circular_slider.dart
Normal file
@ -0,0 +1,148 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_slider_paint.dart';
|
||||
|
||||
/// Returns a widget which displays a circle to be used as a slider.
|
||||
///
|
||||
/// Required arguments are init and end to set the initial selection.
|
||||
/// onSelectionChange is a callback function which returns new values as the user
|
||||
/// changes the interval.
|
||||
/// The rest of the params are used to change the look and feel.
|
||||
///
|
||||
/// DoubleCircularSlider(5, 10, onSelectionChange: () => {});
|
||||
class DoubleCircularSlider extends StatefulWidget {
|
||||
/// the selection will be values between 0..divisions; max value is 300
|
||||
final int divisions;
|
||||
|
||||
/// the initial value in the selection
|
||||
final int init;
|
||||
|
||||
/// the end value in the selection
|
||||
final int end;
|
||||
|
||||
/// the number of primary sectors to be painted
|
||||
/// will be painted using selectionColor
|
||||
final int primarySectors;
|
||||
|
||||
/// the number of secondary sectors to be painted
|
||||
/// will be painted using baseColor
|
||||
final int secondarySectors;
|
||||
|
||||
/// an optional widget that would be mounted inside the circle
|
||||
final Widget child;
|
||||
|
||||
/// height of the canvas, default at 220
|
||||
final double height;
|
||||
|
||||
/// width of the canvas, default at 220
|
||||
final double width;
|
||||
|
||||
/// color of the base circle and sections
|
||||
final Color baseColor;
|
||||
|
||||
/// color of the selection
|
||||
final Color selectionColor;
|
||||
|
||||
/// color of the handlers
|
||||
final Color handlerColor;
|
||||
|
||||
/// callback function when init and end change
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionChange;
|
||||
|
||||
/// callback function when init and end finish
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionEnd;
|
||||
|
||||
/// outter radius for the handlers
|
||||
final double handlerOutterRadius;
|
||||
|
||||
/// if true an extra handler ring will be displayed in the handler
|
||||
final bool showHandlerOutter;
|
||||
|
||||
/// stroke width for the slider, defaults at 12.0
|
||||
final double sliderStrokeWidth;
|
||||
|
||||
/// if true, the onSelectionChange will also return the number of laps in the slider
|
||||
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
|
||||
final bool shouldCountLaps;
|
||||
|
||||
DoubleCircularSlider(
|
||||
this.divisions,
|
||||
this.init,
|
||||
this.end, {
|
||||
this.height,
|
||||
this.width,
|
||||
this.child,
|
||||
this.primarySectors,
|
||||
this.secondarySectors,
|
||||
this.baseColor,
|
||||
this.selectionColor,
|
||||
this.handlerColor,
|
||||
this.onSelectionChange,
|
||||
this.onSelectionEnd,
|
||||
this.handlerOutterRadius,
|
||||
this.showHandlerOutter,
|
||||
this.sliderStrokeWidth,
|
||||
this.shouldCountLaps,
|
||||
}) : assert(init >= 0 && init <= divisions,
|
||||
'init has to be > 0 and < divisions value'),
|
||||
assert(end >= 0 && end <= divisions,
|
||||
'end has to be > 0 and < divisions value'),
|
||||
assert(divisions >= 0 && divisions <= 300,
|
||||
'divisions has to be > 0 and <= 300');
|
||||
|
||||
@override
|
||||
_DoubleCircularSliderState createState() => _DoubleCircularSliderState();
|
||||
}
|
||||
|
||||
class _DoubleCircularSliderState extends State<DoubleCircularSlider> {
|
||||
int _init;
|
||||
int _end;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init = widget.init;
|
||||
_end = widget.end;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.height ?? 220,
|
||||
width: widget.width ?? 220,
|
||||
child: CircularSliderPaint(
|
||||
mode: CircularSliderMode.doubleHandler,
|
||||
init: _init,
|
||||
end: _end,
|
||||
divisions: widget.divisions,
|
||||
primarySectors: widget.primarySectors ?? 0,
|
||||
secondarySectors: widget.secondarySectors ?? 0,
|
||||
child: widget.child,
|
||||
onSelectionChange: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionChange != null) {
|
||||
widget.onSelectionChange(newInit, newEnd, laps);
|
||||
}
|
||||
setState(() {
|
||||
_init = newInit;
|
||||
_end = newEnd;
|
||||
});
|
||||
},
|
||||
onSelectionEnd: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionEnd != null) {
|
||||
widget.onSelectionEnd(newInit, newEnd, laps);
|
||||
}
|
||||
},
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
|
||||
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
|
||||
selectionColor:
|
||||
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
|
||||
handlerColor: widget.handlerColor ?? Colors.white,
|
||||
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
|
||||
showRoundedCapInSelection: false,
|
||||
showHandlerOutter: widget.showHandlerOutter ?? true,
|
||||
shouldCountLaps: widget.shouldCountLaps ?? false,
|
||||
));
|
||||
}
|
||||
}
|
147
lib/plugins/circular_slider/single_circular_slider.dart
Normal file
147
lib/plugins/circular_slider/single_circular_slider.dart
Normal file
@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_slider_paint.dart';
|
||||
|
||||
import '../../utils/logger.dart';
|
||||
|
||||
/// Returns a widget which displays a circle to be used as a slider.
|
||||
///
|
||||
/// Required arguments are position and divisions to set the initial selection.
|
||||
/// onSelectionChange is a callback function which returns new values as the user
|
||||
/// changes the interval.
|
||||
/// The rest of the params are used to change the look and feel.
|
||||
///
|
||||
/// SingleCircularSlider(5, 10, onSelectionChange: () => {});
|
||||
class SingleCircularSlider extends StatefulWidget {
|
||||
/// the selection will be values between 0..divisions; max value is 300
|
||||
final int divisions;
|
||||
|
||||
/// the initial value in the selection
|
||||
int position;
|
||||
|
||||
/// the number of primary sectors to be painted
|
||||
/// will be painted using selectionColor
|
||||
final int primarySectors;
|
||||
|
||||
/// the number of secondary sectors to be painted
|
||||
/// will be painted using baseColor
|
||||
final int secondarySectors;
|
||||
|
||||
/// an optional widget that would be mounted inside the circle
|
||||
final Widget child;
|
||||
|
||||
/// height of the canvas, default at 220
|
||||
final double height;
|
||||
|
||||
/// width of the canvas, default at 220
|
||||
final double width;
|
||||
|
||||
/// color of the base circle and sections
|
||||
final Color baseColor;
|
||||
|
||||
/// color of the selection
|
||||
final Color selectionColor;
|
||||
|
||||
/// color of the handlers
|
||||
final Color handlerColor;
|
||||
|
||||
/// callback function when init and end change
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionChange;
|
||||
|
||||
/// callback function when init and end finish
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionEnd;
|
||||
|
||||
/// outter radius for the handlers
|
||||
final double handlerOutterRadius;
|
||||
|
||||
/// if true will paint a rounded cap in the selection slider start
|
||||
final bool showRoundedCapInSelection;
|
||||
|
||||
/// if true an extra handler ring will be displayed in the handler
|
||||
final bool showHandlerOutter;
|
||||
|
||||
/// stroke width for the slider, defaults at 12.0
|
||||
final double sliderStrokeWidth;
|
||||
|
||||
/// if true, the onSelectionChange will also return the number of laps in the slider
|
||||
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
|
||||
final bool shouldCountLaps;
|
||||
|
||||
SingleCircularSlider(
|
||||
this.divisions,
|
||||
this.position, {
|
||||
this.height,
|
||||
this.width,
|
||||
this.child,
|
||||
this.primarySectors,
|
||||
this.secondarySectors,
|
||||
this.baseColor,
|
||||
this.selectionColor,
|
||||
this.handlerColor,
|
||||
this.onSelectionChange,
|
||||
this.onSelectionEnd,
|
||||
this.handlerOutterRadius,
|
||||
this.showRoundedCapInSelection,
|
||||
this.showHandlerOutter,
|
||||
this.sliderStrokeWidth,
|
||||
this.shouldCountLaps,
|
||||
}) : assert(position >= 0 && position <= divisions,
|
||||
'init has to be > 0 and < divisions value'),
|
||||
assert(divisions >= 0 && divisions <= 300,
|
||||
'divisions has to be > 0 and <= 300');
|
||||
|
||||
@override
|
||||
_SingleCircularSliderState createState() => _SingleCircularSliderState();
|
||||
}
|
||||
|
||||
class _SingleCircularSliderState extends State<SingleCircularSlider> {
|
||||
int _end;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_end = widget.position;
|
||||
Logger.d('Init: _end=$_end');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Logger.d('Build: _end=$_end');
|
||||
return Container(
|
||||
height: widget.height ?? 220,
|
||||
width: widget.width ?? 220,
|
||||
child: CircularSliderPaint(
|
||||
mode: CircularSliderMode.singleHandler,
|
||||
init: 0,
|
||||
end: _end,
|
||||
divisions: widget.divisions,
|
||||
primarySectors: widget.primarySectors ?? 0,
|
||||
secondarySectors: widget.secondarySectors ?? 0,
|
||||
child: widget.child,
|
||||
onSelectionChange: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionChange != null) {
|
||||
widget.onSelectionChange(newInit, newEnd, laps);
|
||||
}
|
||||
setState(() {
|
||||
_end = newEnd;
|
||||
});
|
||||
},
|
||||
onSelectionEnd: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionEnd != null) {
|
||||
widget.onSelectionEnd(newInit, newEnd, laps);
|
||||
}
|
||||
},
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
|
||||
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
|
||||
selectionColor:
|
||||
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
|
||||
handlerColor: widget.handlerColor ?? Colors.white,
|
||||
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
|
||||
showRoundedCapInSelection: widget.showRoundedCapInSelection ?? false,
|
||||
showHandlerOutter: widget.showHandlerOutter ?? true,
|
||||
shouldCountLaps: widget.shouldCountLaps ?? false,
|
||||
));
|
||||
}
|
||||
}
|
77
lib/plugins/circular_slider/slider_painter.dart
Normal file
77
lib/plugins/circular_slider/slider_painter.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_slider_paint.dart' show CircularSliderMode;
|
||||
import 'utils.dart';
|
||||
|
||||
class SliderPainter extends CustomPainter {
|
||||
CircularSliderMode mode;
|
||||
double startAngle;
|
||||
double endAngle;
|
||||
double sweepAngle;
|
||||
Color selectionColor;
|
||||
Color handlerColor;
|
||||
double handlerOutterRadius;
|
||||
bool showRoundedCapInSelection;
|
||||
bool showHandlerOutter;
|
||||
double sliderStrokeWidth;
|
||||
|
||||
Offset initHandler;
|
||||
Offset endHandler;
|
||||
Offset center;
|
||||
double radius;
|
||||
|
||||
SliderPainter({
|
||||
@required this.mode,
|
||||
@required this.startAngle,
|
||||
@required this.endAngle,
|
||||
@required this.sweepAngle,
|
||||
@required this.selectionColor,
|
||||
@required this.handlerColor,
|
||||
@required this.handlerOutterRadius,
|
||||
@required this.showRoundedCapInSelection,
|
||||
@required this.showHandlerOutter,
|
||||
@required this.sliderStrokeWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint progress = _getPaint(color: selectionColor);
|
||||
|
||||
center = Offset(size.width / 2, size.height / 2);
|
||||
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
|
||||
|
||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2 + startAngle, sweepAngle, false, progress);
|
||||
|
||||
Paint handler = _getPaint(color: handlerColor, style: PaintingStyle.fill);
|
||||
Paint handlerOutter = _getPaint(color: handlerColor, width: 2.0);
|
||||
|
||||
// draw handlers
|
||||
if (mode == CircularSliderMode.doubleHandler) {
|
||||
initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);
|
||||
canvas.drawCircle(initHandler, 8.0, handler);
|
||||
canvas.drawCircle(initHandler, handlerOutterRadius, handlerOutter);
|
||||
}
|
||||
|
||||
endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);
|
||||
canvas.drawCircle(endHandler, 8.0, handler);
|
||||
if (showHandlerOutter) {
|
||||
canvas.drawCircle(endHandler, handlerOutterRadius, handlerOutter);
|
||||
}
|
||||
}
|
||||
|
||||
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
|
||||
Paint()
|
||||
..color = color
|
||||
..strokeCap =
|
||||
showRoundedCapInSelection ? StrokeCap.round : StrokeCap.butt
|
||||
..style = style ?? PaintingStyle.stroke
|
||||
..strokeWidth = width ?? sliderStrokeWidth;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
75
lib/plugins/circular_slider/utils.dart
Normal file
75
lib/plugins/circular_slider/utils.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
double percentageToRadians(double percentage) => ((2 * pi * percentage) / 100);
|
||||
|
||||
double radiansToPercentage(double radians) {
|
||||
var normalized = radians < 0 ? -radians : 2 * pi - radians;
|
||||
var percentage = ((100 * normalized) / (2 * pi));
|
||||
// TODO we have an inconsistency of pi/2 in terms of percentage and radians
|
||||
return (percentage + 25) % 100;
|
||||
}
|
||||
|
||||
double coordinatesToRadians(Offset center, Offset coords) {
|
||||
var a = coords.dx - center.dx;
|
||||
var b = center.dy - coords.dy;
|
||||
return atan2(b, a);
|
||||
}
|
||||
|
||||
Offset radiansToCoordinates(Offset center, double radians, double radius) {
|
||||
var dx = center.dx + radius * cos(radians);
|
||||
var dy = center.dy + radius * sin(radians);
|
||||
return Offset(dx, dy);
|
||||
}
|
||||
|
||||
double valueToPercentage(int time, int intervals) => (time / intervals) * 100;
|
||||
|
||||
int percentageToValue(double percentage, int intervals) =>
|
||||
((percentage * intervals) / 100).round();
|
||||
|
||||
bool isPointInsideCircle(Offset point, Offset center, double rradius) {
|
||||
var radius = rradius * 1.2;
|
||||
return point.dx < (center.dx + radius) &&
|
||||
point.dx > (center.dx - radius) &&
|
||||
point.dy < (center.dy + radius) &&
|
||||
point.dy > (center.dy - radius);
|
||||
}
|
||||
|
||||
bool isPointAlongCircle(Offset point, Offset center, double radius) {
|
||||
// distance is root(sqr(x2 - x1) + sqr(y2 - y1))
|
||||
// i.e., (7,8) and (3,2) -> 7.21
|
||||
var d1 = pow(point.dx - center.dx, 2);
|
||||
var d2 = pow(point.dy - center.dy, 2);
|
||||
var distance = sqrt(d1 + d2);
|
||||
return (distance - radius).abs() < 10;
|
||||
}
|
||||
|
||||
double getSweepAngle(double init, double end) {
|
||||
if (end > init) {
|
||||
return end - init;
|
||||
}
|
||||
return (100 - init + end).abs();
|
||||
}
|
||||
|
||||
List<Offset> getSectionsCoordinatesInCircle(
|
||||
Offset center, double radius, int sections) {
|
||||
var intervalAngle = (pi * 2) / sections;
|
||||
return List<int>.generate(sections, (int index) => index).map((i) {
|
||||
var radians = (pi / 2) + (intervalAngle * i);
|
||||
return radiansToCoordinates(center, radians, radius);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool isAngleInsideRadiansSelection(double angle, double start, double sweep) {
|
||||
var normalized = angle > pi / 2 ? 5 * pi / 2 - angle : pi / 2 - angle;
|
||||
var end = (start + sweep) % (2 * pi);
|
||||
return end > start
|
||||
? normalized > start && normalized < end
|
||||
: normalized > start || normalized < end;
|
||||
}
|
||||
|
||||
// this is not 100% accurate but it works
|
||||
// we just want to see if a value changed drastically its value
|
||||
bool radiansWasModuloed(double current, double previous) {
|
||||
return (previous - current).abs() > (3 * pi / 2);
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
part of '../main.dart';
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class Logger {
|
||||
|
||||
|
@ -22,13 +22,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_size_text
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -26,7 +26,6 @@ dependencies:
|
||||
flutter_secure_storage: ^3.2.1+1
|
||||
device_info: ^0.4.0+2
|
||||
flutter_local_notifications: ^0.8.2
|
||||
auto_size_text: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user