152 lines
4.8 KiB
Dart
152 lines
4.8 KiB
Dart
|
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<double> 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;
|
||
|
}
|