Flutter-自定义之钟表

简介: Flutter-自定义之钟表

效果图

绘制控件(四大角色)

  • CustomPaint
  • CustomPainter
  • Canvas
  • Paint
CustomPaint

英文翻译:自定义绘制;CustomPaint可以实现很多酷炫的动画和效果。CustomPaint 提供了让用户自定义widget的能力,它暴露了一个canvas,可以通过这个canvas来绘制widget,CustomPaint会先调用painter绘制背景,然后再绘制child,最后调用foregroundPainter来绘制前景,CustomPaint的定义如下

CustomPaint({Key key, 
      CustomPainter painter,
      CustomPainter foregroundPainter,
      Size size: Size.zero, 
      bool isComplex: false, 
      bool willChange: false, 
      Widget child })
  
  • painter:负责绘制背景的painter
  • foregroundPainter : 负责绘制前景的painter
  • size : 控件大小
  • isComplex : 是否复杂绘制,需要用cache来提高绘制效率
  • willChange : 和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变
  • child : 子widget
CustomPainter

CustomPaint的绘制过程都将会交给CustomPainter来完成,CustomPainter是个抽象接口,在子类继承CustomPainter的时候必须要重写它的paint 跟 shouldRepaint接口。

class MyCustomPainter extends CustomPainter { 
 //绘制自定义的效果  
@override  void paint(Canvas canvas, Size size) {
} 
 //判断是否需要重新绘制ui 通常在当前实例和旧实例属性不一致时返回true。 
 @override  bool shouldRepaint(MyCustomPainter oldDelegate) {   
        return this != oldDelegate; 
 }
}
  • paint : 每当CustomPaint需要重绘的时候都会调用此接口
  • shouldRepaint : 当CustomPaint被重新设置了一个新的painter后会回调此方法,CustomPaint会根据shouldRepaint的返回值来判断是否需要重新绘制ui,譬如新的painter跟旧的painter绘制的内容不一样时,此时shouldRepaint需要返回true来通知CustomPaint重新绘制。
Canvas
  • Canvas:画布,真正的绘制是由canvas跟paint来完成的,画布提供了各种绘制的接口来绘制图形。 常用的绘制接口有更多请查看官方文档
小知识点:
  • canvas.save(); 画布将当前的状态保存
  • canvas.restore(); 画布取出原来所保存的状态
Paint
  • Paint:画笔,是用来在画布上面绘制图形。画笔属性:颜色、线宽、绘制模式、抗锯齿等等。
常用属性有:
  • isAntiAlias : 设置画笔是否扛锯齿
  • color : 颜色
  • strokeWidth : 设置画笔画线宽度
  • style :绘制模式,画线或充满

以上简单介绍了Flutter绘制API,下面开始真正来实现钟表的效果。

实现开始

四大步骤
  • 1、效果功能分析。
  • 2、功能拆解。
  • 3、功能参数。
  • 4、功能代码实现。
1、效果功能分析

钟表(参考自己家的钟表)

2、功能拆解
  • 绘制边框API:
void drawCircle(Offset c, double radius, Paint paint)
  • 绘制刻度:
void drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
  • 绘制数字:
TextPainter(
  textAlign: TextAlign.center,
  textDirection: TextDirection.ltr,
  text: TextSpan(),
) ..layout()
  ..paint(canvas, offset);
  • 绘制时针:
void drawLine(Offset p1, Offset p2, Paint paint)
  • 绘制分针:
void drawLine(Offset p1, Offset p2, Paint paint)
  • 绘制秒针:
void drawLine(Offset p1, Offset p2, Paint paint)
  • 绘制中间圆圈:
void drawCircle(Offset c, double radius, Paint paint)
  • 绘制移动小球:
void drawCircle(Offset c, double radius, Paint paint)
  • 移动指针:
 Timer.periodic(Duration(seconds: 1), (timer) {
    setState(() {
      dateTime = DateTime.now();
    });
});
3、功能参数
  • 钟表的半径
  • 边框的颜色
  • 刻度的颜色
  • 数字的颜色
  • 时针的颜色
  • 分针的颜色
  • 秒针的颜色
  • 走秒小球颜色
  • 中间圆颜色
4、功能代码实现

从功能拆解可以看出,所有的功能未知数就是位置的计算,我们可以先计算下点的坐标。

如图:

我们一起来回顾下上学时期的数学几何知识吧!

  • 先求A点

  • 再求B点

  • 再求C点

同上计算出也是:

C点坐标(R + L*sinα,R - L*cosα)

所以说在园中,随便一个点的坐标都是:

(R + Lsinα,R - Lcosα)

上面计算过程中使用了之前学习的诱导公式,这里列举出来:

诱导公式

到这里基本需要计算的知识点已经完毕了,剩下就是对API的使用,因为代码不多,这里直接把完整的代码贴出来,注释很详细。

完整代码
import 'dart:async';
import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';

///时钟
class ClockView extends StatefulWidget {
  const ClockView({
    Key key,
    this.radius = 150,
    this.borderColor = Colors.black,
    this.scaleColor = Colors.black,
    this.numberColor = Colors.black,
    this.moveBallColor = Colors.red,
    this.hourHandColor = Colors.black,
    this.minuteHandColor = Colors.black,
    this.secondHandColor = Colors.red,
    this.middleCircleColor = Colors.red,
  }) : super(key: key);

  //钟表的半径
  final double radius;

  //边框的颜色
  final Color borderColor;

  //刻度的颜色
  final Color scaleColor;

  //数字的颜色
  final Color numberColor;

  //走秒小球颜色
  final Color moveBallColor;

  //时针的颜色
  final Color hourHandColor;

  //分针的颜色
  final Color minuteHandColor;

  //秒针的颜色
  final Color secondHandColor;

  //中间圆颜色
  final Color middleCircleColor;

  @override
  State<StatefulWidget> createState() {
    return ClockViewState();
  }
}

class ClockViewState extends State<ClockView> {
  //当前时间
  DateTime dateTime;

  //定时器
  Timer timer;

  @override
  void initState() {
    super.initState();
    dateTime = DateTime.now();
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        dateTime = DateTime.now();
      });
    });
  }

  @override
  void dispose() {
    //取消定时器
    if (timer.isActive) {
      timer?.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: ClockPainter(
        dateTime,
        radius: widget.radius,
        borderColor: this.widget.borderColor,
        scaleColor: this.widget.scaleColor,
        numberColor: this.widget.numberColor,
        moveBallColor: this.widget.moveBallColor,
        hourHandColor: this.widget.hourHandColor,
        minuteHandColor: this.widget.minuteHandColor,
        secondHandColor: this.widget.secondHandColor,
        middleCircleColor: this.widget.middleCircleColor,
      ),
      size: Size(widget.radius * 2, widget.radius * 2),
    );
  }
}

class ClockPainter extends CustomPainter {
  //边框的颜色
  final Color borderColor;

  //刻度的颜色
  final Color scaleColor;

  //数字的颜色
  final Color numberColor;

  //中间圆颜色
  final Color middleCircleColor;

  //走秒小球颜色
  final Color moveBallColor;

  //时针的颜色
  final Color hourHandColor;

  //分针的颜色
  final Color minuteHandColor;

  //秒针的颜色
  final Color secondHandColor;

  //边框画笔的宽度
  double borderWidth;

  //刻度画笔的宽度
  double scaleWidth;

  //数字画笔的宽度
  double numberWidth;

  //时针画笔的宽度
  double hourHandWidth;

  //分针画笔的宽度
  double minuteHandWidth;

  //秒针画笔的宽度
  double secondHandWidth;

  //中间圆的宽度
  double middleCircleWidth;

  //小刻度的位置集合
  List<Offset> scaleOffset = [];

  //大刻度的位置集合 每5个小刻度是一个大刻度
  List<Offset> bigScaleOffset = [];

  //钟表的半径
  final double radius;

  //当前时间
  final DateTime dateTime;

  //边框画笔
  Paint borderPaint;

  //刻度画笔
  Paint scalePaint;

  //大刻度画笔
  Paint biggerScalePaint;

  //数字画笔
  TextPainter textPainter;

  //时针画笔
  Paint hourPaint;

  //分针画笔
  Paint minutePaint;

  //秒针画笔
  Paint secondPaint;

  //中间圆画笔
  Paint centerPaint;

  //移动小球画笔
  Paint moveBallPaint;

  ClockPainter(
    this.dateTime, {
    this.radius,
    this.borderColor,
    this.scaleColor,
    this.numberColor,
    this.moveBallColor,
    this.hourHandColor,
    this.minuteHandColor,
    this.secondHandColor,
    this.middleCircleColor,
  }) {
    //根据自己的审美设置这些画笔的宽度
    borderWidth = 8 * (radius / 100);
    scaleWidth = 2 * (radius / 100);
    numberWidth = 20 * (radius / 100);
    hourHandWidth = 5 * (radius / 100);
    minuteHandWidth = 3 * (radius / 100);
    secondHandWidth = 1 * (radius / 100);
    middleCircleWidth = 4 * (radius / 100);

    //边框画笔
    borderPaint =
        createPaint(borderColor, borderWidth, style: PaintingStyle.stroke);
    //刻度画笔
    scalePaint = createPaint(numberColor, scaleWidth);
    //大刻度
    biggerScalePaint = createPaint(numberColor, scaleWidth * 2);
    //时针画笔
    hourPaint = createPaint(hourHandColor, hourHandWidth);
    //分针画笔
    minutePaint = createPaint(minuteHandColor, minuteHandWidth);
    //秒针画笔
    secondPaint = createPaint(secondHandColor, secondHandWidth);
    //中间圆
    centerPaint = createPaint(middleCircleColor, middleCircleWidth);
    //移动小球画笔
    moveBallPaint = createPaint(moveBallColor, scaleWidth * 2);
    //数字
    textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );

    //计算出 小刻度和大刻度
    final l = radius - borderWidth * 2;
    for (var i = 0; i < 60; i++) {
      Offset offset = pointOffset(radius, l, 360 / 60 * i);
      //小刻度
      scaleOffset.add(offset);
      //大刻度
      if (i % 5 == 0) {
        bigScaleOffset.add(offset);
      }
    }
  }

  @override
  void paint(Canvas canvas, Size size) {
    //绘制边框
    drawBorder(canvas);
    //绘制刻度
    drawScale(canvas);
    //绘制数字
    drawNumber(canvas);
    //绘制时针
    drawHour(canvas);
    //绘制分针
    drawMinute(canvas);
    //绘制秒针
    drawSecond(canvas);
    //绘制中间圆圈
    drawMiddleCircle(canvas);
    //绘制移动小球
    drawMoveBall(canvas);
  }

  //判断是否需要重绘
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }

  ///绘制边框
  void drawBorder(Canvas canvas) {
    canvas.drawCircle(
        Offset(radius, radius), radius - borderWidth / 2, borderPaint);
  }

  ///绘制刻度
  void drawScale(Canvas canvas) {
    //小刻度
    canvas.drawPoints(PointMode.points, scaleOffset, scalePaint);
    //大刻度
    canvas.drawPoints(PointMode.points, bigScaleOffset, biggerScalePaint);
  }

  ///绘制数字
  void drawNumber(Canvas canvas) {
    double l = radius - borderWidth * 4;
    for (var i = 0; i < bigScaleOffset.length; i++) {
      textPainter.text = TextSpan(
        text: "${i == 0 ? 12 : i}",
        style: TextStyle(color: numberColor, fontSize: numberWidth),
      );
      Offset offset = pointOffset(radius, l, i * 360 / 12);
      textPainter.layout();
      textPainter.paint(
          canvas,
          Offset(
            offset.dx - (textPainter.width / 2),
            offset.dy - (textPainter.height / 2),
          ));
    }
  }

  ///绘制时针
  void drawHour(Canvas canvas) {
    final hour = dateTime.hour;
    double angle = 360 / 12 * hour + dateTime.minute / 60 * 30;
    Offset hourHand1 = pointOffset(radius, radius * 0.1, angle + 180);
    Offset hourHand2 = pointOffset(radius, radius * 0.45, angle);
    canvas.drawLine(hourHand1, hourHand2, hourPaint);
  }

  ///绘制分针
  void drawMinute(Canvas canvas) {
    final minute = dateTime.minute;
    double angle = 360 / 60 * minute + dateTime.second / 60 * 6;
    Offset minuteHand1 = pointOffset(radius, radius * 0.1, angle + 180);
    Offset minuteHand2 = pointOffset(radius, radius * 0.7, angle);
    canvas.drawLine(minuteHand1, minuteHand2, minutePaint);
  }

  ///绘制秒针
  void drawSecond(Canvas canvas) {
    final second = dateTime.second;
    double angle = 360 / 60 * second;
    Offset secondHand1 = pointOffset(radius, radius * 0.1, angle + 180);
    Offset secondHand2 = pointOffset(radius, radius * 0.7, angle);
    canvas.drawLine(secondHand1, secondHand2, secondPaint);
  }

  ///绘制中间圆圈
  void drawMiddleCircle(Canvas canvas) {
    canvas.drawCircle(Offset(radius, radius), middleCircleWidth, centerPaint);
  }

  ///绘制移动小球
  void drawMoveBall(Canvas canvas) {
    final second = dateTime.second;
    canvas.drawCircle(scaleOffset[second], middleCircleWidth, moveBallPaint);
  }
}

///创建Paint
Paint createPaint(Color color, double strokeWidth,
    {PaintingStyle style = PaintingStyle.fill}) {
  return Paint()
    ..color = color
    ..isAntiAlias = true
    ..style = style
    ..strokeCap = StrokeCap.round
    ..strokeWidth = strokeWidth;
}

///圆中万能求点公式
Offset pointOffset(double radius, double l, double angle) {
  return Offset(
    radius + l * sin(degToRad(angle)),
    radius - l * cos(degToRad(angle)),
  );
}

///角度转换为弧度
num degToRad(num deg) => deg * (pi / 180.0);


相关文章
|
19天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
84 1
|
21天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
116 1
|
21天前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
66 1
|
21天前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
68 1
|
21天前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
69 1
|
2月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
84 3
|
3月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
98 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
3月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
61 0
|
3月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
19 0
|
3月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
89 0