.NET MAUI是Xamarin的进化版,如果你已经用了Xamarin,那么可以尝试把工程移植到.NET MAUI。这篇文章分享下我的移植心得。
从Xamarin.Forms迁移到.NET MAUI
我之前用Xamarin.Forms写了一个适用于Android和iOS的一维码,二维码扫描程序:https://github.com/yushulx/xamarin-forms-barcode-qrcode-scanner。
微软官方在GitHub上发布了一个移植教程,但我觉得还是创建一个新的.NET MAUI比较好,这样可以避免一下子出现大量的编译错误。
创建.NET MAUI工程
要创建.NET MAUI工程,你需要安装Visual Studio 2022 Preview版本,稳定版不包含工程模板。
.NET MAUI工程创建之后,包含了Windows, macOS, Tizen, Android和iOS的代码和支持框架。为了简洁干净,删除不必要的平台,只保留Android和iOS。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-android;net6.0-ios</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>BarcodeQrScanner</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
</Project>
通过NuGet安装依赖库
我们需要用到两个重要的库,一个是SkiaSharp,另一个是Dynamsoft Barcode Reader。
SkiaSharp用于Xamarin.Forms和.NET MAUI的包名是不同的。使用Xamarin.Forms安装SkiaSharp.Views.Forms ,使用.NET MAUI安装SkiaSharp.Views.Maui.Controls 。
Dynamsoft Barcode Reader没有针对框架定制。
安装之后发现,Android可以用,而iOS会出现兼容性错误。
因此移植之后,Android上可以正常扫码,而iOS上只能打开摄像头。iOS上相关的扫码代码全部注释掉了,这部分可以用别的库替代,或者等待SDK发布更新。
代码
根据在线文档,要调用平台相关的代码需要用到partical class 和partial method 。
我们在公共代码里定义:
public partial class BarcodeQRCodeService
{
public partial void InitSDK(string license);
public partial BarcodeQrData[] DecodeFile(string filePath);
}
然后在Android和iOS的平台代码里去实现接口。
public class DBRLicenseVerificationListener : Java.Lang.Object, IDBRLicenseVerificationListener
{
public void DBRLicenseVerificationCallback(bool isSuccess, Java.Lang.Exception error)
{
if (!isSuccess)
{
System.Console.WriteLine(error.Message);
}
}
}
public partial class BarcodeQRCodeService
{
public partial void InitSDK(string license)
{
BarcodeReader.InitLicense(license, new DBRLicenseVerificationListener());
reader = new BarcodeReader();
}
public partial BarcodeQrData[] DecodeFile(string filePath)
{
BarcodeQrData[] output = null;
try
{
PublicRuntimeSettings settings = reader.RuntimeSettings;
settings.ExpectedBarcodesCount = 512;
reader.UpdateRuntimeSettings(settings);
TextResult[] results = reader.DecodeFile(filePath);
if (results != null && results.Length > 0)
{
output = new BarcodeQrData[results.Length];
int index = 0;
foreach (TextResult result in results)
{
BarcodeQrData data = new BarcodeQrData();
data.text = result.BarcodeText;
data.format = result.BarcodeFormatString;
LocalizationResult localizationResult = result.LocalizationResult;
data.points = new SKPoint[localizationResult.ResultPoints.Count];
int pointsIndex = 0;
foreach (Com.Dynamsoft.Dbr.Point point in localizationResult.ResultPoints)
{
SKPoint p = new SKPoint();
p.X = point.X;
p.Y = point.Y;
data.points[pointsIndex++] = p;
}
output[index++] = data;
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
return output;
}
}
以上这部分代码用于拍照识别一维码,二维码。
要实现视频流扫码,需要用到Custom Renderer。这部分可以完全参照Xamarin.Forms,不同的是namepsace 需要修改。
Android
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Controls.Compatibility.Platform.Android.FastRenderers;
[assembly: ExportRenderer(typeof(BarcodeQrScanner.CameraPreview), typeof(CameraPreviewRenderer))]
namespace BarcodeQrScanner.Platforms.Android
{
public class CameraPreviewRenderer : FrameLayout, IVisualElementRenderer, IViewRenderer, TextureView.ISurfaceTextureListener, Camera.IPreviewCallback, Handler.ICallback
{
CameraPreview element;
VisualElementTracker visualElementTracker;
VisualElementRenderer visualElementRenderer;
int? defaultLabelFor;
public event EventHandler<VisualElementChangedEventArgs> ElementChanged;
public event EventHandler<PropertyChangedEventArgs> ElementPropertyChanged;
BarcodeReader barcodeReader = new BarcodeReader();
CameraPreview Element
{
get => element;
set
{
if (element == value)
{
return;
}
var oldElement = element;
element = value;
OnElementChanged(new ElementChangedEventArgs<CameraPreview>(oldElement, element));
}
}
public CameraPreviewRenderer(Context context) : base(context)
{
visualElementRenderer = new VisualElementRenderer(this);
}
void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
{
if (e.OldElement != null)
{
e.OldElement.PropertyChanged -= OnElementPropertyChanged;
}
if (e.NewElement != null)
{
this.EnsureId();
e.NewElement.PropertyChanged += OnElementPropertyChanged;
ElevationHelper.SetElevation(this, e.NewElement);
}
ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement));
try
{
SetupUserInterface();
AddView(view);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(@" ERROR: ", ex.Message);
}
}
void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
ElementPropertyChanged?.Invoke(this, e);
}
}
iOS
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Controls.Handlers.Compatibility;
[assembly: ExportRenderer(typeof(BarcodeQrScanner.CameraPreview), typeof(CameraPreviewRenderer))]
namespace BarcodeQrScanner.Platforms.iOS
{
public class CameraPreviewRenderer : ViewRenderer<CameraPreview, UICameraPreview>
{
UICameraPreview uiCameraPreview;
protected override void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
uiCameraPreview.Tapped -= OnCameraPreviewTapped;
}
if (e.NewElement != null)
{
if (Control == null)
{
uiCameraPreview = new UICameraPreview(e.NewElement);
SetNativeControl(uiCameraPreview);
}
uiCameraPreview.Tapped += OnCameraPreviewTapped;
}
}
void OnCameraPreviewTapped(object sender, EventArgs e)
{
if (uiCameraPreview.IsPreviewing)
{
uiCameraPreview.CaptureSession.StopRunning();
uiCameraPreview.IsPreviewing = false;
}
else
{
uiCameraPreview.CaptureSession.StartRunning();
uiCameraPreview.IsPreviewing = true;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Control.CaptureSession.Dispose();
Control.Dispose();
}
base.Dispose(disposing);
}
}
}
在Content Page中使用SkiaSharp来绘制overlay。对应的namespace 要改成xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls" 。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
xmlns:local="clr-namespace:BarcodeQrScanner;assembly=BarcodeQrScanner"
x:Class="BarcodeQrScanner.CameraPage"
Title="CameraPage">
<Grid x:Name="scannerView" Margin="0">
<local:CameraPreview
x:Name="cameraView"
Camera="Rear"
ScanMode="Multiple"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
ResultReady="CameraPreview_ResultReady"/>
<Label FontSize="18"
FontAttributes="Bold"
TextColor="Blue"
x:Name="ResultLabel"
Text=" "
HorizontalOptions="Center"
VerticalOptions="Center" />
<skia:SKCanvasView x:Name="canvasView"
Margin="0"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
</Grid>
</ContentPage>
最后还有一步是在MauiProgram.cs 中配置SkiaSharp 和Custom Renderer 。
using Microsoft.Maui.Controls.Compatibility.Hosting;
using SkiaSharp.Views.Maui.Controls.Hosting;
namespace BarcodeQrScanner;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseSkiaSharp()
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
}).UseMauiCompatibility()
.ConfigureMauiHandlers((handlers) => {
#if ANDROID
handlers.AddCompatibilityRenderer(typeof(CameraPreview), typeof(BarcodeQrScanner.Platforms.Android.CameraPreviewRenderer));
#endif
#if IOS
handlers.AddHandler(typeof(CameraPreview), typeof(BarcodeQrScanner.Platforms.iOS.CameraPreviewRenderer));
#endif
});
return builder.Build();
}
}
少了这部分代码,程序运行时会报错。这里比Xamarin.Forms繁琐。
.NET MAUI疑似Bug
移植过程中发现了两个问题。
-
iOS的AVCaptureDevice.DevicesWithMediaType 返回值是null 。摄像头获取的代码改用AVCaptureDevice[] videoDevices = AVCaptureDevice.Devices; 。再从中选择后置摄像头。 -
Label 中的文本显示不全,似乎受到默认text 字符串长度影响。如果默认字符串长度小于结果长度,结果会被切割。所以临时的解决方案就是在text 中设置很长的空格。 <Label FontSize="18"
FontAttributes="Bold"
x:Name="ResultLabel"
Text=" "
TextColor="Red"
HorizontalOptions="Center"
VerticalOptions="Center"/>
程序演示
源码
https://github.com/yushulx/maui-barcode-qrcode-scanner
|