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

该文详细介绍了如何在.NETMAU框架中利用正弦曲线绘制MaterialYou风格的时钟小部件,包括锯齿表盘的绘制、指针的动态旋转以及沿路径的文本绘制,提供了完整的代码示例和时钟效果展示。


谷歌在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<Point>();
    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

C#的闹钟应用程序。到处都是注释,一看就懂! 核心代码和效果展示见我的博客:http://blog.youkuaiyun.com/luochao5862426/article/details/78570141 个人特色: 1、可以这么所说,别人有的,我有!别人没有的,我也有。集百家之长,选我就对了,嘿嘿。 2、代码详细,基本上每一条稍微重要点的代码都有注释这行代码是干嘛的,所以你可以看到好多好多注释,详细的不能再详细! 3、里面包含,可直接导入的项目文件、该程序的PPT展示以及录制的视频展示。 4、模块清晰,注释详细,低耦合,高内聚。 主页面介绍:分三个部分 一、动态时钟部分,像石英钟一样时、分、秒针不停转动。 二、定点闹钟部分,简单点就是可以定闹钟。 三、闹钟备忘录部分,显而易见,为了添加提示功能。 本人设计了两种可选模式: 1、懒人模式(可多次延时响铃,下面主要讲述这个模式) 2、生存模式(本次考验失败后则下次的闹钟提前几分钟响铃。由于时间有限本人没去实现这个功能) 主要功能介绍: 一、时钟(石英钟) 1、使用C#的GDI+画出石英钟时、分、秒针不停转动的效果并加上了指针的尾巴。 二、闹钟 1、定闹钟时添加备注。 2、自选(默认铃声或本地铃声)试听铃声。所以机智的你可以当一个MP3用了。 3、设定多个闹钟。重点是,你可以设置不同类型(今天、每天、自定义星期、指定日期)的闹钟。 4、设定不同的响铃方式。包括:只响一次、不断响铃、静音响铃。 5、定时关机。定闹钟的时候选择了定时关机这个选项,那么,在闹钟到点后的一定时间内(我设置的3秒)会自动关机。 6、开机自启动。这个可以自己设定,很多人不需要。 7、响铃抖屏。闹钟到点后会抖动一小段时间(我设置的3秒)的屏幕,并同步跳到你打开的所有窗口的最顶层窗体。 8、系统托盘。可以隐藏到系统托盘。 三、备忘录 {备忘录组成:时段+时间+备注+尾巴(可删除,知识为了查看有哪些操作)} 1、移除所定的闹钟。 2、把闹钟备忘录保存至本地。 3、从本地导入至闹钟备忘录。所以你可以在本地修改备忘录咯,包括时间和内容。 4、修改闹钟备忘录内容。在程序界面修改备忘录。 5、查找备忘录内容。在程序界面查找备忘录内容。 6、显示倒计时。你在定闹钟的时候要是选了倒计时这个选项,则你可以在备忘录里面选中,显示倒计时。
<think> 首先,用户的问题是基于系统级指令的。系统级指令包括:所有行内数学表达式必须使用$...$格式,独立公式用$$...$$格式单独成段,确保LaTeX语法正确,使用中文回答,生成相关问题,并在回答中引用的段落末尾自然地添加引用标识如[^1]。 用户的问题是:“我想查找C# MAUI进行蓝牙开发的代码 请问C# MAUI蓝牙开发代码示例”。这意味着用户想要C# MAUI中进行蓝牙开发的代码示例。 回顾用户提供的引用: - 引用[1]:关于MASA MAUI Plugin IOS蓝牙低功耗的实现,参考Swift代码。 - 引用[2]:Xamarin & MAUI蓝牙插件介绍。 - 引用[3]:MASA MAUI Plugin安卓蓝牙低功耗扫描的实现。 - 引用[4]:Maui开发使用蓝牙BLE在不同操作系统的权限配置。 - 引用[5]:包括避坑指南、性能优化等。 用户说这些引用仅供参考,并不是用户真正的需求,而是参考用户上一次的问题和我的回答。用户在指令中说:“上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。” 在上面的对话中,用户的第一个问题是要求系统级指令,然后现在的问题是关于C# MAUI蓝牙开发代码。用户没有提供上一次的问题和回答历史,所以我需要基于当前上下文。 在系统级指令的示例中,有关于快速排序的示例,但我应该忽略那个,专注于用户当前的问题。 用户要求代码示例,所以我需要提供一个C# MAUI的蓝牙开发代码示例。从引用中,我看到有MASA MAUI Plugin的提及,特别是引用[1]和[3]涉及IOS和安卓的蓝牙实现。 引用[1]是关于IOS的BLE实现,参考Swift代码。引用[3]是关于安卓的BLE扫描。 引用[2]提到Xamarin & MAUI蓝牙插件。 引用[4]是关于权限配置。 引用[5]是关于布局和性能问题。 所以,为了回答用户的问题,我应该提供一个C# MAUI的蓝牙开发代码示例。既然用户提到“C# MAUI”,我需要确保代码是用C#MAUI应用。 从引用中,MASA MAUI Plugin似乎是相关的。引用[3]说:“MASA MAUI Plugin 安卓蓝牙低功耗(一)蓝牙扫描。” 它有开发步骤和Java代码,但用户想要C#代码。引用中提到“本文JAVA相关代码均来自安卓开发者官网”,但对于MAUI,可能需要在C#中实现。 实际上,MAUI允许使用C#跨平台开发,所以蓝牙功能可能需要使用平台特定的API或插件。 引用[2]说:“Xamarin & MAUI蓝牙插件为开发者提供了一个强大且易用的BLE解决方案”,所以可能有一个蓝牙插件。 用户的问题直接是“C# MAUI蓝牙开发代码示例”,所以我应该提供一个简单的代码示例。 理想情况下,我应该基于引用构建回答。例如,引用[3]提到在安卓上实现蓝牙扫描,我可以提供一个C#的简化版本。 回想一下蓝牙低功耗(BLE)在MAUI中的实现。 由于MAUI是跨平台的,蓝牙访问通常需要处理平台差异。常用的方法是使用依赖服务或插件。 从引用[4],有权限配置的示例。 所以,我的回答应该包括: 1. **介绍**:简要解释如何在MAUI中实现蓝牙开发。 2. **代码示例**:提供一个简单的C#代码示例,比如扫描蓝牙设备。因为引用中提到扫描功能。 3. **引用集成**:在回答中自然添加引用标识。例如,提到权限配置时引用[4]。 4. **结构**:确保回答清晰,逐步解决问题。 5. **生成相关问题**:在回答结束后,添加“§§相关问题§§”部分,列出几个相关问题。 现在,构建回答: - **开头**:使用中文回答。解释用户可以使用插件来实现。 - **代码示例**:提供一个简单的蓝牙扫描代码。既然引用[3]有安卓的扫描,我可以提供一个通用的MAUI代码,使用假设的插件或基于Xamarin.Essentials,因为MAUI集成了类似功能。Xamarin.Essentials有蓝牙LE功能。 实际上,Xamarin.Essentials有BluetoothLE类,但可能不是完整的。或者使用社区插件如Plugin.BLE。 在引用[2]中提到“Xamarin & MAUI蓝牙插件”,所以我可以基于那个。 为简单起见,我将提供一个使用Plugin.BLE的示例,因为它是一个流行的插件。 代码示例: ```csharp using Xamarin.Essentials; using Plugin.BLE; using Plugin.BLE.Abstractions.Contracts; public async Task ScanForDevices() { var ble = CrossBluetoothLE.Current; var adapter = CrossBluetoothLE.Current.Adapter; adapter.DeviceDiscovered += (s, e) => { Console.WriteLine($"Device discovered: {e.Device.Name}"); }; await adapter.StartScanningForDevicesAsync(); } ``` 但在MAUI中,需要处理权限等。 引用[4]有权限配置,所以提到它。 在回答中,添加引用。 最后,生成相关问题。 结构化回答: 1. **介绍蓝牙开发在MAUI中**:使用插件简化开发。 2. **权限配置**:引用[4]中的信息。 3. **代码示例**:扫描蓝牙设备。 4. **简要解释**。 5. **结尾**:鼓励尝试。 然后,相关问题。 确保LaTeX:用户问题中没有涉及数学,所以可能不需要,但如果提到公式,要使用$...$。这里可能不需要。 引用标识:添加如[^4]。 生成相关问题例子: 1. 如何在MAUI中处理蓝牙权限? 2. MAUI蓝牙开发的常见问题有哪些? 3. 如何优化MAUI中的蓝牙性能? 基于引用[5],有避坑指南。 完整回答草稿: 在C# MAUI中进行蓝牙开发,可以使用社区插件如Plugin.BLE来简化蓝牙低功耗(BLE)操作。以下是一个简单的蓝牙扫描代码示例: 首先,确保在项目中安装Plugin.BLE NuGet包。 ```csharp using Plugin.BLE; using Plugin.BLE.Abstractions.Contracts; using System.Collections.ObjectModel; using System.ComponentModel; public class BluetoothViewModel : INotifyPropertyChanged { private readonly IBluetoothLE ble; private readonly IAdapter adapter; public ObservableCollection<IDevice> Devices { get; } = new ObservableCollection<IDevice>(); public BluetoothViewModel() { ble = CrossBluetoothLE.Current; adapter = CrossBluetoothLE.Current.Adapter; adapter.DeviceDiscovered += (sender, e) => { if (!Devices.Contains(e.Device)) { Devices.Add(e.Device); OnPropertyChanged(nameof(Devices)); } }; } public async Task StartScanningAsync() { await adapter.StartScanningForDevicesAsync(); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } ``` 在MAUI中,还需要配置权限。例如,在iOS的Info.plist中添加蓝牙权限描述[^4]。 权限配置示例: ```xml <key>NSBluetoothAlwaysUsageDescription</key> <string>需要蓝牙权限以连接设备</string> ``` 开发过程中,注意处理跨平台差异,如引用[5]中提到的布局和性能问题。 §§相关问题§§ 1. 如何在MAUI中请求蓝牙权限? 2. MAUI蓝牙开发中常见的错误有哪些? 3. 如何连接和通信蓝牙设备在MAUI中? 在回答中,自然地添加引用:[^4] for permissions, and perhaps [^5] for pitfalls if mentioned. 用户提到“参考用户上一次的问题和你的回答”,但在这个对话中,上一次是系统指令设置,没有实际对话历史。所以,基于当前问题。 </think> ### C# MAUI蓝牙开发代码示例 在C# MAUI中进行蓝牙低功耗(BLE)开发时,通常使用社区插件简化跨平台实现。Plugin.BLE(也称为Xamarin.BLE)是一个流行的NuGet包,支持iOS、Android等平台。以下基于引用内容,提供一个完整的蓝牙扫描功能示例,涵盖初始化、权限配置和设备扫描步骤。 #### 1. **项目设置与权限配置** 在MAUI项目中,首先安装Plugin.BLE NuGet包(版本2.1.1+)。然后,配置平台特定权限: - **Android**:在`Platforms/Android/AndroidManifest.xml`添加蓝牙权限: ```xml <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Android 12+需要位置权限 --> ``` - **iOS**:在`Platforms/iOS/Info.plist`中添加蓝牙后台模式和使用描述(参考引用[^4]): ```xml <key>UIBackgroundModes</key> <array> <string>bluetooth-central</string> </array> <key>NSBluetoothAlwaysUsageDescription</key> <string>需要蓝牙权限以连接设备</string> ``` #### 2. **C#代码示例:蓝牙扫描** 以下是一个简单的ViewModel类,实现蓝牙设备扫描功能。代码使用Plugin.BLE库处理设备发现逻辑: ```csharp using Plugin.BLE; // 导入Plugin.BLE库 using Plugin.BLE.Abstractions.Contracts; // 接口定义 using System.Collections.ObjectModel; using System.ComponentModel; public class BluetoothScannerViewModel : INotifyPropertyChanged { private readonly IBluetoothLE _ble; // BLE实例 private readonly IAdapter _adapter; // 蓝牙适配器 public ObservableCollection<IDevice> DiscoveredDevices { get; } = new ObservableCollection<IDevice>(); // 存储发现的设备 public BluetoothScannerViewModel() { _ble = CrossBluetoothLE.Current; // 获取当前平台BLE实例 _adapter = CrossBluetoothLE.Current.Adapter; // 获取适配器 // 设备发现事件处理 _adapter.DeviceDiscovered += (sender, e) => { if (!DiscoveredDevices.Contains(e.Device)) { DiscoveredDevices.Add(e.Device); OnPropertyChanged(nameof(DiscoveredDevices)); // 通知UI更新 } }; // 扫描状态变化日志(可选) _adapter.ScanTimeoutElapsed += (sender, e) => Console.WriteLine("扫描超时"); } // 开始扫描方法 public async Task StartScanningAsync() { if (!_ble.IsAvailable || _ble.State != BluetoothState.On) { Console.WriteLine("蓝牙不可用或未开启"); return; } DiscoveredDevices.Clear(); // 清空旧设备列表 await _adapter.StartScanningForDevicesAsync(); // 启动扫描(默认扫描10秒) } // 停止扫描方法 public async Task StopScanningAsync() { await _adapter.StopScanningForDevicesAsync(); } // INotifyPropertyChanged实现 public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } ``` #### 3. **在MAUI页面中使用** 在MAUI的XAML页面(如`MainPage.xaml`)中绑定ViewModel并添加扫描按钮: ```xml <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="YourNamespace.MainPage"> <StackLayout> <Button Text="开始扫描" Clicked="OnScanClicked" /> <ListView ItemsSource="{Binding DiscoveredDevices}"> <ListView.ItemTemplate> <DataTemplate> <TextCell Text="{Binding Name}" Detail="{Binding Id}" /> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage> ``` 后台代码(`MainPage.xaml.cs`)初始化ViewModel: ```csharp public partial class MainPage : ContentPage { private readonly BluetoothScannerViewModel _viewModel = new BluetoothScannerViewModel(); public MainPage() { InitializeComponent(); BindingContext = _viewModel; // 绑定ViewModel } private async void OnScanClicked(object sender, EventArgs e) { await _viewModel.StartScanningAsync(); // 调用扫描方法 } } ``` #### 4. **注意事项与优化** - **权限处理**:在运行时动态请求权限(如Android的`Permissions.RequestAsync<Permissions.LocationWhenInUse>()`)。 - **跨平台问题**:iOS需确保后台模式正确配置以避免扫描中断[^4]。 - **性能建议**:避免在主线程执行耗时操作(如设备连接),参考引用[^5]的动画优化方法,使用异步任务。 - **错误处理**:添加`try-catch`块处理蓝牙状态异常(如用户拒绝权限)。 开发中如遇布局问题(如iOS/Android渲染不一致),可采用平台特定样式文件解决[^5]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林晓lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值