diff --git a/lib/cards/card_widget.dart b/lib/cards/card_widget.dart index bb34833..5143248 100644 --- a/lib/cards/card_widget.dart +++ b/lib/cards/card_widget.dart @@ -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 body = []; body.add(CardHeader(name: card.name ?? "")); diff --git a/lib/cards/widgets/light_card_body.dart b/lib/cards/widgets/light_card_body.dart new file mode 100644 index 0000000..db260e2 --- /dev/null +++ b/lib/cards/widgets/light_card_body.dart @@ -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 { + + @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, + ), + ); + } + ), + ) + ] + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/const.dart b/lib/const.dart index 197b2a5..4906313 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -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 { diff --git a/lib/main.dart b/lib/main.dart index d4897d4..171877f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); diff --git a/lib/plugins/circular_slider/base_painter.dart b/lib/plugins/circular_slider/base_painter.dart new file mode 100644 index 0000000..a29afa6 --- /dev/null +++ b/lib/plugins/circular_slider/base_painter.dart @@ -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 inits, List 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; + } +} \ No newline at end of file diff --git a/lib/plugins/circular_slider/circular_slider_paint.dart b/lib/plugins/circular_slider/circular_slider_paint.dart new file mode 100644 index 0000000..7251070 --- /dev/null +++ b/lib/plugins/circular_slider/circular_slider_paint.dart @@ -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 = 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 onSelectionChange; + final SelectionChanged 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 { + 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: { + CustomPanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => 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) {} +} \ No newline at end of file diff --git a/lib/plugins/circular_slider/double_circular_slider.dart b/lib/plugins/circular_slider/double_circular_slider.dart new file mode 100644 index 0000000..24710ee --- /dev/null +++ b/lib/plugins/circular_slider/double_circular_slider.dart @@ -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 onSelectionChange; + + /// callback function when init and end finish + /// (int init, int end) => void + final SelectionChanged 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 { + 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, + )); + } +} \ No newline at end of file diff --git a/lib/plugins/circular_slider/single_circular_slider.dart b/lib/plugins/circular_slider/single_circular_slider.dart new file mode 100644 index 0000000..8963705 --- /dev/null +++ b/lib/plugins/circular_slider/single_circular_slider.dart @@ -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 onSelectionChange; + + /// callback function when init and end finish + /// (int init, int end) => void + final SelectionChanged 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 { + 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, + )); + } +} \ No newline at end of file diff --git a/lib/plugins/circular_slider/slider_painter.dart b/lib/plugins/circular_slider/slider_painter.dart new file mode 100644 index 0000000..d0930ca --- /dev/null +++ b/lib/plugins/circular_slider/slider_painter.dart @@ -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; + } +} \ No newline at end of file diff --git a/lib/plugins/circular_slider/utils.dart b/lib/plugins/circular_slider/utils.dart new file mode 100644 index 0000000..8458f79 --- /dev/null +++ b/lib/plugins/circular_slider/utils.dart @@ -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 getSectionsCoordinatesInCircle( + Offset center, double radius, int sections) { + var intervalAngle = (pi * 2) / sections; + return List.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); +} \ No newline at end of file diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 717afe2..466ddcc 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -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 { diff --git a/pubspec.lock b/pubspec.lock index 85f9665..98da7b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 6c3a7cc..b9b1c96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: