import 'dart:math'; import 'package:flutter/material.dart'; class Compass extends StatelessWidget { static const double fallbackSize = 100.0; final double degree; final Color pointerColor; final double pointerThickness; final double relativeCircleSize; final double relativePointerLength; final double? height; final double? width; const Compass( {this.degree = 0.0, this.pointerColor = Colors.red, this.pointerThickness = 0.2, this.relativeCircleSize = 0.8, this.relativePointerLength = 1.0, this.width, this.height, Key? key}) : assert(pointerThickness > 0, "Pointer must be thicker than zero"), assert(relativeCircleSize >= 0 && relativeCircleSize <= 1.0, "Relative circle size must be between 0.0 and 1.0"), assert(relativePointerLength >= 0 && relativePointerLength <= 1.0, "Relative circle size must be between 0.0 and 1.0"), super(key: key); /// Determines widget's width and height /// Both will be equal to the smallest length which is either /// set as [width], [height] or as [constraints] maxWidth/maxHeight. /// [fallbackSize] will be used if none of them is set. double calcSideLength( double? setWidth, double? setHeight, BoxConstraints constraints) { List sizes = [ setWidth ?? double.infinity, setHeight ?? double.infinity, constraints.maxWidth, constraints.maxHeight, ]; var tmp = sizes.where((s) => s != double.infinity); return tmp.isNotEmpty ? tmp.reduce(min) : fallbackSize; } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { var sideLength = calcSideLength(width, height, constraints); return SizedBox( height: sideLength, width: sideLength, child: CustomPaint( painter: _Compass( degree: degree, pointerColor: pointerColor, pointerThickness: pointerThickness, relativeCircleSize: relativeCircleSize, relativePointerLength: relativePointerLength), ), ); }, ); } /// Creates a 6x6 grid with Compass widgets. /// The look of each widget differs from its predecessor /// - Orientation: 0 - 350° /// - Color: from green over brown to red /// - Size of circle: 0-87,5% of widgets width/height /// - Pointers thickness: 5-35% of widgets width/height static Widget demo() { return Container( color: Colors.grey, width: 300, height: 300, child: GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 50, childAspectRatio: 1, crossAxisSpacing: 0, mainAxisSpacing: 0), itemCount: 36, itemBuilder: (BuildContext ctxt, int index) => CustomPaint( painter: _Compass( degree: index * 10, relativeCircleSize: (index * 0.025), pointerThickness: index / 120 + 0.05, pointerColor: Color.fromRGBO((index * 255 / 36).floor(), 255 - (index * 255 / 36).floor(), 20, 1), ), ), ), ); } } class _Compass extends CustomPainter { static const Offset origin = Offset(0, 0); static final Paint black = Paint()..color = Colors.black; static final Paint white = Paint()..color = Colors.white; final double degree; final Color pointerColor; final double pointerThickness; final double relativeCircleSize; final double relativePointerLength; _Compass({ this.degree = 0.0, this.pointerColor = Colors.red, this.pointerThickness = 0.2, this.relativeCircleSize = 0.8, this.relativePointerLength = 1.0, }); @override void paint(Canvas canvas, Size size) { final Paint pointerPaint = Paint()..color = pointerColor; double minSide = size.width < size.height ? size.width : size.height; double circleRadius = minSide * relativeCircleSize / 2; double pointerLength = minSide * relativePointerLength / 2; double pointerRadius = minSide * pointerThickness / 2; canvas.save(); canvas.translate(size.width / 2, size.height / 2); canvas.drawCircle(origin, circleRadius, black); canvas.drawCircle(origin, circleRadius * 0.9, white); canvas.drawCircle(origin, pointerRadius, pointerPaint); canvas.rotate(degree * pi / 180); canvas.drawPath(makeTriangle(pointerRadius, pointerLength), pointerPaint); canvas.restore(); } Path makeTriangle(double halfBase, double height) { var path = Path(); path.moveTo(-halfBase, 0); path.lineTo(0, -height); path.lineTo(halfBase, 0); path.close(); return path; } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }