如何使用.NET MAUI开发Android扫码应用

.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-iosTargetFrameworks>
		<OutputType>ExeOutputType>
		<RootNamespace>BarcodeQrScannerRootNamespace>
		<UseMaui>trueUseMaui>
		<SingleProject>trueSingleProject>
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没有针对框架定制。

如何使用.NET MAUI开发Android扫码应用_第1张图片
安装之后发现,Android可以用,而iOS会出现兼容性错误。

如何使用.NET MAUI开发Android扫码应用_第2张图片
因此移植之后,Android上可以正常扫码,而iOS上只能打开摄像头。iOS上相关的扫码代码全部注释掉了,这部分可以用别的库替代,或者等待SDK发布更新。

代码

根据在线文档,要调用平台相关的代码需要用到partical classpartial 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)
            {
                // Unsubscribe
                uiCameraPreview.Tapped -= OnCameraPreviewTapped;
            }
            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    uiCameraPreview = new UICameraPreview(e.NewElement);
                    SetNativeControl(uiCameraPreview);
                }
                // Subscribe
                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"


<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中配置SkiaSharpCustom 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

你可能感兴趣的:(android,.net,ios,MAUI)