前言
最近收到了一朋友求助,希望幫忙使用Flutter開發(fā)一個組件,實現(xiàn)特定App上調(diào)節(jié)參數(shù)功能功能組件,要求:
1.扇形動畫調(diào)節(jié)有動畫效果使用起來絲滑順暢
2.有一定的邊界效果
之前開發(fā)過類似圖形,以及自定義繪制相關(guān)的開發(fā),所以難度不是很大,且看下本人實現(xiàn)后最終效果,總的來說朋友很是滿意,在此也貢獻(xiàn)出來希望可以幫有需要的朋友。
代碼地址
horizon.gif
vertical.gif
需求分析
難點(diǎn)分析點(diǎn)線區(qū)域高亮其實這些都是比較簡單,主要的難點(diǎn)終點(diǎn)在哪里,例如橫向起點(diǎn)坐標(biāo)x=0,終點(diǎn)坐標(biāo),縱坐標(biāo)很容易確定就是和起點(diǎn)一樣的y, 不好確定是x;
那么怎么辦呢,仔細(xì)觀察圓弧是落在圓周上的,這就很簡單了利用勾股定理就很容易解決了,思路有了下面就是上代碼了。還有一些就是坐標(biāo)的轉(zhuǎn)化,自適應(yīng)適配,下面列出實現(xiàn)代碼希望可以幫助到需要朋友
代碼實現(xiàn)
實現(xiàn)上來說并不復(fù)雜,大部分代碼都是在處理繪制相關(guān)的工作
typedef ValueChanged = void Function(double value);
class SliderHorizontal extends StatefulWidget {
/// size 是畫布大小 實際寬度還需要要比這個大 imgWidth
final double size;
//取值范圍 0-100
final double value;
//可取最小值
final double minValue;
//可取最大值
final double maxValue;
final ValueChanged? onChanged;
final SliderController? sliderController;
const SliderHorizontal(
{super.key,
required this.size,
required this.value,
required this.minValue,
required this.maxValue,
this.onChanged,
this.sliderController})
: assert(maxValue > minValue),
assert(value >= minValue),
assert(value <= maxValue);
@override
State<SliderHorizontal> createState() => _SliderHorizontalState();
}
class _SliderHorizontalState extends State<SliderHorizontal> {
double get imgWidth {
return 77.gr;
}
double get imgLeft {
return 15.gr;
}
//當(dāng)前所在值 取值范圍0-100
double currentValue = 0;
double get imgDx {
var rate =
(currentValue - widget.minValue) / (widget.maxValue - widget.minValue);
return widget.size * 0.2 - 20.gr + rate * (widget.size * 0.6);
// * currentValue / 100 - 20.gr;
}
@override
void initState() {
super.initState();
RatioUtil.init(size: widget.size, designSize: 300);
currentValue = widget.value;
widget.sliderController?.addStep = addStep;
widget.sliderController?.subStep = subStep;
}
void addStep() {
onChanged(currentValue + 0.1);
}
void subStep() {
onChanged(currentValue - 0.1);
}
void onChanged(double value) {
if (value < widget.minValue) {
value = widget.minValue;
}
if (value > widget.maxValue) {
value = widget.maxValue;
}
widget.onChanged?.call(value);
setState(() {
currentValue = value;
});
}
@override
Widget build(BuildContext context) {
var size = widget.size;
return SizedBox(
width: size + imgWidth,
height: size + imgWidth,
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: EdgeInsets.only(left: imgWidth, top: imgWidth),
child: CustomPaint(
size: Size(size, size),
painter: VerticalPainter(
minValue: widget.minValue,
maxValue: widget.maxValue,
value: currentValue),
),
),
Positioned(left: 0, bottom: imgDx, child: _buildImg()),
Positioned(
height: 0.6 * size,
bottom: 0.2 * size,
left: 0,
right: 0,
child: RotatedBox(
quarterTurns: 3,
child: _buildSlider(),
),
),
],
),
);
}
Widget _buildSlider() {
return BetterCupertinoSlider(
min: widget.minValue,
max: widget.maxValue,
value: currentValue,
configure: BetterCupertinoSliderConfigure(
trackHorizontalPadding: 0.gr,
trackHeight: 20.gr,
trackLeftColor: Colors.transparent,
trackRightColor: Colors.transparent,
thumbRadius: 20.gr,
thumbPainter: (canvas, rect) {
final RRect rrect = RRect.fromRectAndRadius(
rect,
Radius.circular(rect.shortestSide / 2.0),
);
canvas.drawRRect(rrect, Paint()..color = Colors.transparent);
},
),
onChanged: onChanged,
);
}
Widget _buildImg() {
return Stack(
children: [
Image.asset(
Assets.images.horizontalHandle.path,
width: imgWidth,
),
Positioned.fill(
left: imgLeft,
right: 0,
bottom: 0,
top: 0,
child: Container(
alignment: Alignment.center,
child: Text(currentValue.toStringAsFixed(1),
style: TextStyle(color: Colors.white, fontSize: 16.gr)),
),
)
],
);
}
}
class VerticalPainter extends CustomPainter {
final double maxValue;
final double minValue;
final double value;
VerticalPainter(
{required this.maxValue, required this.minValue, required this.value});
TextPainter getTextPainter(String text) {
return TextPainter(
text: TextSpan(text: text, style: TextStyle(fontSize: 16.gr)),
textDirection: TextDirection.rtl,
)..layout(minWidth: 0, maxWidth: 20);
}
@override
void paint(Canvas canvas, Size size) {
final double axisLength = size.width;
final Offset origin = Offset(0, size.height);
final Paint axisPaint = Paint()
..color = Colors.white
..strokeWidth = 5.0.gr
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round
..strokeCap = StrokeCap.round;
final hPainter = getTextPainter('H');
hPainter.paint(canvas, origin + Offset(-5, -axisLength - 30));
final qPainter = getTextPainter('Q');
qPainter.paint(canvas, origin + Offset(axisLength + 10, -10));
final Path xyPath = Path()
..moveTo(0, 0)
..lineTo(0, origin.dy)
..lineTo(origin.dy, origin.dy);
canvas.drawPath(xyPath, axisPaint);
// final zeroPainter = getTextPainter('0');
// zeroPainter.paint(canvas, Offset(-20, origin.dy - 10));
//0.2 的位置點(diǎn)
final realLength = 0.6 * axisLength;
final minPainter = getTextPainter(minValue.toStringAsFixed(0));
final minDy = origin.dy - 0.2 * axisLength;
minPainter.paint(canvas, Offset(-25, minDy - 8.gr));
final maxPainter = getTextPainter(maxValue.toStringAsFixed(0));
final maxDy = origin.dy - 0.8 * axisLength;
maxPainter.paint(canvas, Offset(-25, maxDy - 8.gr));
// 繪制從X軸到Y(jié)軸的頂點(diǎn)的弧形
final double arcRadius = axisLength;
final Rect arcRect = Rect.fromCircle(center: origin, radius: arcRadius);
final Paint arcPaint = Paint()
..color = const Color(0x99FFFFFF)
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
canvas.drawArc(arcRect, -pi / 2, pi / 2, false, arcPaint);
final Path rectPath = Path()
..moveTo(calculateB(axisLength, axisLength - minDy), minDy)
..lineTo(0, minDy)
..lineTo(0, maxDy)
..lineTo(calculateB(axisLength, axisLength - maxDy), maxDy)
..arcToPoint(Offset(calculateB(axisLength, axisLength - minDy), minDy),
radius: Radius.circular(axisLength));
final Paint rectPaint = Paint()
..color = const Color(0x61FFFFFF)
..style = PaintingStyle.fill;
canvas.drawPath(rectPath, rectPaint);
final Paint strokePaint = Paint()
..color = Colors.white
..strokeWidth = 1
..style = PaintingStyle.stroke;
canvas.drawPath(rectPath, strokePaint);
//百分比
final rate = (value - minValue) / (maxValue - minValue);
final double posY = axisLength - (0.2 * axisLength + rate * realLength);
final startPoint = Offset(calculateB(axisLength, axisLength - posY), posY);
final endPoint = Offset(0, posY);
final Paint soildPaint = Paint()
..color = Colors.white
..strokeWidth = 4.0.gr;
final Paint soildPaint1 = Paint()
..color = const Color(0x61FFFFFF)
..strokeWidth = 12.0.gr;
// 桿子
canvas.drawLine(startPoint, endPoint, soildPaint1);
canvas.drawLine(startPoint, endPoint, soildPaint);
//圓頭
canvas.drawOval(
Rect.fromCircle(center: startPoint, radius: 8.gr), soildPaint);
canvas.drawOval(
Rect.fromCircle(center: startPoint, radius: 12.gr), soildPaint1);
}
double calculateB(double c, double a) {
return sqrt(c * c - a * a);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}