Flutter Drawing Collection | Drawing of Second Dial

Work together to create and grow together! This is the 5th day of my participation in the "Nuggets Daily New Plan·August Update Challenge", click to view the details of the event


foreword

The purpose of this article is to draw exercises, which will be included in the FlutterUnit Drawing Collection. In addition, there is a chapter in "Flutter Grammar Basics - The Land of Dreams" that needs to use this dial, but does not want to involve too much drawing knowledge, so it is implemented here. The effect is as follows, the outer ring is a dial surrounded by lines, a small circle inside indicates the current position, and information text is displayed in the middle.


1. Demand analysis

What is drawn here is 秒表表盘that a circle is 1 分种, and there are corresponding 3grids , that is to say, there are a total 180of grids, and the angle between each grid is . Let's take a look at the parameters required in the drawing process. First, we need an Durationobject that represents the current time of the stopwatch. In addition, the angle of the small circle can be calculated according to the time.

Parameters that can be configured when drawing, such as radius, scale color, text color, style, etc. In addition, more detailed parameters such as the length, thickness, and radius of the small circle of the scale can be calculated according to the radius.


2. Scale drawing

The square as shown below is the drawing area, and the left scale width is determined scaleLineWidthby the width and _kScaleWidthRatescale .

const double _kScaleWidthRate = 0.4/10;
final double scaleLineWidth = size.width*_kScaleWidthRate;
复制代码

Use a straight line when drawing the scale, as long as you determine the coordinates of the left and right points. tag1The coordinates of the scale can be easily calculated by moving the origin of the coordinates to the center of the area as tag2shown below.

final Paint scalePainter = Paint();
@override
void paint(Canvas canvas, Size size) {
  canvas.translate(size.width/2, size.height/2); // tag1
  scalePainter..color = Colors.red..style=PaintingStyle.stroke;
  final double scaleLineWidth = size.width*_kScaleWidthRate;
  canvas.drawLine(
    Offset(size.width/2, 0), // tag2
    Offset(size.width/2 - scaleLineWidth, 0), 
    scalePainter
  );
}
复制代码

Let's draw the dial below. We have analyzed a total 180of , which can be drawn by traversing and rotating. As follows, traverse the bar scale above the drawing 180times , and the canvas rotates after each drawing is completed , so that after drawing 180times , the canvas will 360°return to its original position. The content drawn on the interface is displayed as follows:

for(int i = 0; i < 180 ; i++){
  canvas.drawLine(
    Offset(size.width/2, 0), 
    Offset(size.width/2 - scaleLineWidth, 0), 
    scalePainter
  );
  canvas.rotate(pi/180*2);
}
复制代码

3. Text drawing

可以看出这里的文字有两种样式,毫秒数颜色跟随主题色。在 Canvas 文字绘制时可以通过 TextPainter 对象完成。使用该对象必须指定 textDirection ,表示文字的排布方向。

TextPainter textPainter = TextPainter(
  textAlign: TextAlign.center,
  textDirection: TextDirection.ltr,
);
复制代码

textPainter 绘制内容通过 text 成员指定,该成员类型是 InlineSpan 。所以可以展示富文本,如下代码展示 commonStylehighlightStyle 两种样式的文字。

void drawText(Canvas canvas){
  textPainter.text = TextSpan(
      text: '00:04',
      style: commonStyle,
      children: [TextSpan(text: ".65", style: highlightStyle)]);
  textPainter.layout(); // 进行布局
  final double width = textPainter.size.width;
  final double height = textPainter.size.height;
  textPainter.paint(canvas, Offset(-width / 2, -height / 2));
}
复制代码

然后,可以根据一个 duration 对象来得到需要展示的文字:

final Duration duration = Duration(minutes: 0, seconds: 4, milliseconds: 650);

int minus = duration.inMinutes % 60;
int second = duration.inSeconds % 60;
int milliseconds = duration.inMilliseconds % 1000;
String commonStr = '${minus.toString().padLeft(2, "0")}:${second.toString().padLeft(2, "0")}';
String highlightStr = ".${(milliseconds ~/ 10).toString().padLeft(2, "0")}";
复制代码

4.绘制指示器

圆的指示器的半径也是根据大圆半径计算的,然后根据时长计算出偏转角度即可。

final double scaleLineWidth = size.width * _kScaleWidthRate;
final double indicatorRadius = size.width * _kIndicatorRadiusRate;
canvas.drawCircle(
    Offset(0,-size.width/2+scaleLineWidth+indicatorRadius,),
    indicatorRadius/2, 
    indicatorPainter
);
复制代码

下面来算个简单的数学题,已知当前时长,如何求得该时长在表盘的旋转角度?
只要算出当前分钟内毫秒数一分钟毫秒数(60 * 1000) 占比即可。在绘制指示器时,将画布进行旋转 radians 弧度,不过要注意,为了避免这个旋转变换对其他绘制的影响,需要通过 saverestore 方法进行处理。

int second = duration.inSeconds % 60;
int milliseconds = duration.inMilliseconds % 1000;
double radians = (second * 1000 + milliseconds) / (60 * 1000) * 2 * pi;

canvas.save();
canvas.rotate(radians);
// 绘制...
canvas.restore();
复制代码

这样,给出一个 Duration 对象,就能线数处正确的文字及指示器位置:


5. 组件的封装

组件的封装是为了更简洁的使用,如下通过为 StopWatchWidget 组件提供配置即可呈现出对应的绘制效果。就可以将绘制的细节封装起来使用者不需要了解具体是怎么画出来的,只要用 StopWatchWidget 组件即可。

StopWatchWidget(
  duration: Duration(minutes: 0, seconds: 24, milliseconds: 850),
  radius: 100,
),
复制代码

如下在 StopWatchPainter 中封装了4 个可配置的参数,在 shouldRepaint 方法中,当这四者其中之一发生变化时都允许进行重绘。

class StopWatchPainter extends CustomPainter {
  final Duration duration; // 时长
  final Color themeColor; // 主题色
  final Color scaleColor; // 刻度色
  final TextStyle textStyle; // 文本样式
  StopWatchPainter({
    required this.duration,
    required this.themeColor,
    required this.scaleColor,
    required this.textStyle,
  })
    
  // 绘制略...
 
  @override
  bool shouldRepaint(covariant StopWatchPainter oldDelegate) {
    return oldDelegate.duration != duration ||
        oldDelegate.textStyle != textStyle ||
        oldDelegate.themeColor != themeColor||
        oldDelegate.scaleColor != scaleColor;
  }
}
复制代码

StopWatchWidgetInherited from StatelessWidget, itself does not undertake the ability to change state. That is to say, its presentation content is only related to the configuration information passed in by the user, and does not actively change the presentation effect. The drawn artboard is displayed through the CustomPaintcomponent StopWatchPainter.

In addition, pay attention to some small details. The组件 construction parameters are similar to those of , but the components are in direct contact with and. Considering its ease of use, it is necessary to provide some default parameters, or obtain some information according to the current theme. The artboard object is responsible for creation, the roles they face are different, and the considerations for encapsulation are also different.画板使用者创造者

class StopWatchWidget extends StatelessWidget {
  final double radius;
  final Duration duration;
  final Color? themeColor;
  final TextStyle? textStyle;
  final Color scaleColor;

  const StopWatchWidget({
    Key? key,
    required this.radius,
    required this.duration,
    this.scaleColor = const Color(0xffDADADA),
    this.textStyle,
    this.themeColor
  }) : super(key: key);

  TextStyle get commonStyle => TextStyle(
    fontSize: radius/3,
    fontWeight: FontWeight.w200,
    color: const Color(0xff343434),
  );

  @override
  Widget build(BuildContext context) {
    TextStyle style = textStyle??commonStyle;
    Color themeColor = this.themeColor??Theme.of(context).primaryColor;
    return CustomPaint(
      painter: StopWatchPainter(
          duration: duration,
          themeColor: themeColor,
          scaleColor: scaleColor,
          textStyle: style),
      size: Size(radius * 2, radius * 2),
    );
  }
}
复制代码

That's it for this article, and the next article will be based on this drawing component to realize the function of starting and pausing the second dial. Thanks for watching~

Guess you like

Origin juejin.im/post/7137081179315896350