• [MAUI]用纯C#代码写两个漂亮的时钟


    @


    谷歌在2021年5月份推出的Android 12给我们带来了新的UI设计规范Material You,你是否已经体验到了Material You设计的魅力了呢?

    在原生主屏幕启动器中,有一个时钟小部件。这个小部件可以选择表盘风格。

    在这里插入图片描述

    图:Android 12的时钟小部件

    今天挑战在.NET MAU中实现这个Material You风格时钟。

    最终效果如下:

    在这里插入图片描述

    时钟1

    绘制锯齿表盘

    锯齿表盘是正玄波曲线闭合成一个圆形。
    创建Clock1,打开Xaml文件

    在页面布局中创建一个Path对象,设置Stroke和Fill属性。

    <Path Grid.Row="0"
            Grid.Column="1"
            Stroke="white"
            Fill="#FFEED9"
            IsVisible="true"
            x:Name="ModulatedPath">
        <Path.Data>
            <PathGeometry>
                <PathGeometry.Figures>
                    <PathFigureCollection>
                        <PathFigure IsClosed="True" x:Name="MainPathFigure">
                            <PathFigure.Segments>
                                <PathSegmentCollection x:Name="MainPathSegmentCollection">
    
                                PathSegmentCollection>
                            PathFigure.Segments>
                        PathFigure>
    
                    PathFigureCollection>
                PathGeometry.Figures>
            PathGeometry>
        Path.Data>
    Path>
    

    在codebehind中,订阅SizeChanged事件,当控件大小发生变化时,重新绘制表盘。

    public Clock1()
    {
        InitializeComponent();
        this.SizeChanged+=ContentView_SizeChanged;
    }
    

    正弦曲线绘制原理

    单位圆中的正弦函数在平面直角坐标系中可以映射为一个波形曲线,下图所示在0-2π范围中y=sin(x)的图像。

    在这里插入图片描述

    其中最低点和最高点决定了波形的振幅,他们与平衡点的距离即单位圆的半径

    设置变量 r为平衡点,r2为最高点,r3为最低点。

    centerX和centerY为圆心坐标。
    segemts为绘制的线段数,越大锯齿越密集。

    private void ContentView_SizeChanged(object sender, EventArgs e)
    {
    
        var length = (float)Math.Min(this.Width, this.Height) * 0.95;
        var centerX = (float)this.Width / 2;
        var centerY = (float)this.Height / 2;
        var points = new List();
        var r = length / 2;
        var r2 = r * 1.1;
        var r3 = r * 0.9;
        var index = 0;
        var segments = 40;
    }
    

    首先计算各平衡点在圆周上的离散分布坐标(x,y)

    根据index的奇偶性,给与当前点最高点或最低点的半径。

    根据各分配的半径计算调整后的离散点坐标

    在这里插入图片描述

    代码如下:

    ...
    for (var i = 0; i < segments; i += 2)
    {
        var x = r * Math.Cos(i * 2 * Math.PI / segments) + centerX;
        var y = r * Math.Sin(i * 2 * Math.PI / segments) + centerY;
    
        points.Add(new Point((float)x, (float)y));
        var currentR = index++ % 2 == 0 ? r2 : r3;
        x = currentR * Math.Cos((i + 1) * 2 * Math.PI / segments) + centerX;
        y = currentR * Math.Sin((i + 1) * 2 * Math.PI / segments) + centerY;
    
        points.Add(new Point((float)x, (float)y));
    }
    
    

    如此,我们得到一个闭合的锯齿圆形表盘

    在这里插入图片描述

    绘制指针

    在表盘上绘制时钟指针,需要计算时针、分针、秒针的角度,然后根据角度旋转画布,绘制指针。

    秒针每秒钟转动6度,

    分针每分钟转动6度,并叠加每秒0.1度。

    时针每小时转动30度。并叠加每分钟0.5度。

    其中时针和分针由宽度为15的实心填充圆角线条构成

    DateTime dateTime = DateTime.Now;
    
    // Hour hand
    strokePaint.Color = SKColor.Parse("#5E4000");
    strokePaint.StrokeWidth = 15;
    canvas.Save();
    canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
    canvas.DrawLine(0, 0, 0, -r*(float)0.4, strokePaint);
    canvas.Restore();
    
    // Minute hand
    strokePaint.Color = SKColor.Parse("#9C6D00");
    canvas.Save();
    canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
    canvas.DrawLine(0, 0, 0, -r*(float)0.8, strokePaint);
    canvas.Restore();
    
    // Second hand
    strokePaint.Color = SKColor.Parse("#657E3F");
    canvas.Save();
    canvas.RotateDegrees(6 * dateTime.Second);
    strokePaint.StrokeWidth *=(float)0.5;
    strokePaint.Style=SKPaintStyle.Fill;
    canvas.DrawCircle(0, -r*(float)0.8, strokePaint.StrokeWidth, strokePaint);
    canvas.Restore();
    

    指针效果如下:

    在这里插入图片描述

    其中秒针需要绘制一个点,在其以圆心为中心的对侧绘制一个带有日期的文本

    绘制沿路径文本

    首先绘制文本路径,它是一个圆弧,在初始状态圆弧的角度为20度,圆弧的起始角度为70度,终止角度为110度。

    var pathAngle = 20;
    var startAngle = 90-pathAngle;
    var sweepAngle = pathAngle*2;
    var rect = new SKRect(-r*(float)0.8, -r*(float)0.8, r*(float)0.8, r*(float)0.8);
    
    

    使用SkiaSharp的DrawTextOnPath方法绘制沿路径的文本,详情请查看官方文档

    using (SKPath path = new SKPath())
    {
        path.AddArc(rect, startAngle, sweepAngle);
        canvas.DrawTextOnPath(dateStr, path, new SKPoint(), strokePaint);
    }
    

    指针效果如下:

    在这里插入图片描述

    时钟1的完整代码如下:

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;
    
        canvas.Clear();
    
        using (SKPaint strokePaint = new SKPaint())
        using (SKPaint fillPaint = new SKPaint())
        {
            strokePaint.Style = SKPaintStyle.Stroke;
            strokePaint.StrokeCap = SKStrokeCap.Round;
    
            fillPaint.Style = SKPaintStyle.Fill;
            fillPaint.Color = SKColors.Transparent;
    
            // Transform for 100-radius circle centered at origin
            var r = 100f;
            canvas.Translate(info.Width / 2f, info.Height / 2f);
            canvas.Scale(Math.Min(info.Width / 200f, info.Height / 200f));
    
    
            DateTime dateTime = DateTime.Now;
    
            // Hour hand
            strokePaint.Color = SKColor.Parse("#5E4000");
            strokePaint.StrokeWidth = 15;
            canvas.Save();
            canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
            canvas.DrawLine(0, 0, 0, -r*(float)0.4, strokePaint);
            canvas.Restore();
    
            // Minute hand
            strokePaint.Color = SKColor.Parse("#9C6D00");
            canvas.Save();
            canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
            canvas.DrawLine(0, 0, 0, -r*(float)0.62, strokePaint);
            canvas.Restore();
    
            // Second hand
            strokePaint.Color = SKColor.Parse("#657E3F");
            canvas.Save();
            canvas.RotateDegrees(6 * dateTime.Second);
            strokePaint.StrokeWidth *=(float)0.5;
            strokePaint.Style=SKPaintStyle.Fill;
    
            canvas.DrawCircle(0, -r*(float)0.8, strokePaint.StrokeWidth, strokePaint);
    
            strokePaint.Color = SKColors.Black;
            strokePaint.StrokeWidth = 1;
            var dateStr = dateTime.ToString("M");
            var pathAngle = 20;
            var startAngle = 90-pathAngle;
            var sweepAngle = pathAngle*2;
            var rect = new SKRect(-r*(float)0.8, -r*(float)0.8, r*(float)0.8, r*(float)0.8);
    
            using (SKPath path = new SKPath())
            {
                path.AddArc(rect, startAngle, sweepAngle);
    
                //canvas.DrawPath(path, strokePaint);
    
                canvas.DrawTextOnPath(dateStr, path, new SKPoint(), strokePaint);
            }
    
    
            canvas.Restore();
        }
    }
    
    
    

    配置一个定时器,每秒刷新各指针位置

    IDispatcherTimer _timer;
    
    public Clock1()
    {
       
        ...
        this.SizeChanged+=ContentView_SizeChanged;
        _timer=  Dispatcher.CreateTimer();
        _timer.Interval=TimeSpan.FromSeconds(1);
        _timer.Tick+=_timer_Tick;
        _timer.Start();
    }
    
    private void _timer_Tick(object sender, EventArgs e)
    {
        this.canvasView?.InvalidateSurface();
    }
    
    

    时钟1 效果如下:

    在这里插入图片描述

    时钟2

    URWGeometricBlack字体文件放到Fonts目录下

    在这里插入图片描述

    在MauiProgram.cs中注册字体

    .ConfigureFonts(fonts =>
    {
        ...
        fonts.AddFont("URWGeometricBlack.otf", "URWGeometricBlack");
    
    });
    
    

    绘制表盘

    时钟2的表盘相对简单,是一个简单的圆配简洁抽象的数字刻度组成
    创建Clock2,打开Xaml文件,代码如下:

    <Grid>
    
        <Ellipse Grid.Row="0"
                    Grid.Column="1"
                    Stroke="white"
                    Fill="#FFEED9"
                    IsVisible="true"
                    HeightRequest="200"
                    WidthRequest="200"
                    x:Name="ModulatedPath">
        Ellipse>
        <Grid TranslationY="-15">
            <Grid.RowDefinitions>
                <RowDefinition>RowDefinition>
                <RowDefinition>RowDefinition>
            Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition>ColumnDefinition>
                <ColumnDefinition>ColumnDefinition>
            Grid.ColumnDefinitions>
            <Label   Grid.Row="0"
                        Grid.ColumnSpan="2"
                        Text="12"
                        Style="{StaticResource ClockPlateNumberLabelStyle}">Label>
    
            <Label   Grid.RowSpan="2"
                        Grid.Column="1"
                        Text="3"
                        Style="{StaticResource ClockPlateNumberLabelStyle}">Label>
    
            <Label   Grid.Row="1"
                        Grid.ColumnSpan="2"
                        Text="6"
                        Style="{StaticResource ClockPlateNumberLabelStyle}">Label>
    
            <Label   Grid.RowSpan="2"
                        Grid.Column="0"
                        Text="9"
                        Style="{StaticResource ClockPlateNumberLabelStyle}">Label>
    
        Grid>
    
    
    
        <forms:SKCanvasView x:Name="canvasView"
                            PaintSurface="OnCanvasViewPaintSurface" />
    
        <Label FontSize="28"
                HorizontalOptions="Center"
                VerticalOptions="Center"
                x:Name="labelView">Label>
    Grid>
    
    

    同样我们需要放置SKCanvasView对象用于绘制指针

    其中ClockPlateNumberLabelStyle定义如下:

    <Style TargetType="Label"
            x:Key="ClockPlateNumberLabelStyle">
        <Setter Property="HorizontalTextAlignment"
                Value="Center">Setter>
        <Setter Property="VerticalTextAlignment"
                Value="Center">Setter>
        <Setter Property="VerticalOptions"
                Value="Center">Setter>
        <Setter Property="FontAttributes"
                Value="Bold">Setter>
        <Setter Property="FontSize"
                Value="120">Setter>
        <Setter Property="TextColor"
                Value="#F9BC49">Setter>
        <Setter Property="FontFamily"
                Value="URWGeometricBlack">Setter>
    Style>
    

    效果如下:

    在这里插入图片描述

    绘制指针

    时钟2的指针绘制原理与时钟1类似,此处将不赘述,完整代码如下:

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;
    
        canvas.Clear();
    
        using (SKPaint strokePaint = new SKPaint())
        using (SKPaint fillPaint = new SKPaint())
        {
            strokePaint.Style = SKPaintStyle.Stroke;
            strokePaint.StrokeCap = SKStrokeCap.Round;
    
            fillPaint.Style = SKPaintStyle.Fill;
            fillPaint.Color = SKColors.Transparent;
    
            // Transform for 100-radius circle centered at origin
            var r = 100f;
            canvas.Translate(info.Width / 2f, info.Height / 2f);
            canvas.Scale(Math.Min(info.Width / 200f, info.Height / 200f));
    
    
            DateTime dateTime = DateTime.Now;
    
            // Hour hand
            strokePaint.Color = SKColor.Parse("#5E4000");
            strokePaint.StrokeWidth = 15;
            canvas.Save();
            canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
            canvas.DrawLine(0, 0, 0, -r*(float)0.4, strokePaint);
            canvas.Restore();
    
            // Minute hand
            strokePaint.Color = SKColor.Parse("#9C6D00");
            strokePaint.StrokeWidth = 5;
            canvas.Save();
            canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
            canvas.DrawLine(0, 0, 0, -r*(float)0.8, strokePaint);
            canvas.Restore();
    
            // Second hand
            strokePaint.Color = SKColor.Parse("#657E3F");
            strokePaint.StrokeWidth = 2;
            canvas.Save();
            canvas.RotateDegrees(6 * dateTime.Second);
            canvas.DrawLine(0, r*(float)0.1, 0, -r*(float)0.8, strokePaint);
            strokePaint.Style=SKPaintStyle.Fill;
            canvas.DrawCircle(0, 0, 5, strokePaint);
    
            canvas.Restore();
        }
    }
    
    

    时钟2 效果如下

    在这里插入图片描述

    项目地址

    Github:maui-samples

  • 相关阅读:
    谷粒商城 高级篇 (一) --------- ElasticSearch 的简介与安装
    ubuntu 客服端同步ntp服务器时间
    ACL-IJCAI-SIGIR顶级会议论文报告会(AIS 2022)笔记2:分析与可解释性
    内网穿透的应用-如何本地部署Elasticsearch搜索分析引擎实现并发布公网远程访问
    c语言获取文件的md5值
    springcloud、springboot、springcloudalibaba版本依赖关系
    (3) OpenCV图像处理kNN近邻算法
    Java学习笔记6.2.1 字符流 - 文件字符输入流与文件字符输出流
    搭建双主双从的MySQL主从同步复制
    7-15 计算圆周率(分数 15)
  • 原文地址:https://www.cnblogs.com/jevonsflash/p/17519792.html