MAUI Blazor学习6-扫描二维码
MAUI Blazor学习6-扫描二维码
MAUI Blazor系列目录
- MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习5-BLE低功耗蓝牙 - SunnyTrudeau - 博客园 (cnblogs.com)
扫描二维码是很常见的需求,Xamarin平台有一个非常成熟的ZXing.Net.Mobile开源库,但是它依赖Xamarin.Forms,不支持MAUI。这个库的作者又发布了ZXing.Net.Maui,专用于MAUI,使用上也比较简单。但是我写的ZXing.Net.Maui测试DEMO,扫码识别速度很慢,甚至完全无法识别二维码。我下载了https://github.com/redth/ZXing.Net.Maui的源代码,其中有一个BigIslandBarcode项目,测试结果也不行。我更换过手机测试结果仍然不行,但是同样的手机,用ZXing.Net.Mobile的测试代码是可以快速扫描到结果的,目前还不清楚问题在哪里。
对于MAUI Blazor项目而言,还有另外一个扫描二维码的方案,基于浏览器的JavaScript库。这类资源还不少,我对JavaScript不熟,找了大佬们封装好的razor组件库ZXingBlazor来用,非常感谢大佬们的无私奉献。
ZXing Blazor 扫码组件 , ssr/wasm通用 - AlexChow - 博客园 (cnblogs.com)
https://github.com/densen2014/ZXingBlazor
申请权限
在MaBlaApp项目NuGet安装ZXingBlazor。
<PackageReference Include="ZXingBlazor" Version="0.2.7" />
在安卓项目AndroidManifest.xml申请相机权限。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\AndroidManifest.xml
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
如果是在MAUI Xaml中使用相机,这样写已经足够。但是如果想在MAUI Blazor的网页上使用相机,这样是不够的!还要把浏览器和安卓系统的权限进行转换,非常复杂,只能对着大佬们写好的代码抄。
MAUI Blazor 权限经验分享 (定位,使用相机) - AlexChow - 博客园 (cnblogs.com)
D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\PermissionManagingBlazorWebChromeClient.cs
/// <summary> /// 浏览器权限对接安卓权限 /// https://github.com/MackinnonBuck/MauiBlazorPermissionsExample /// </summary> internal class PermissionManagingBlazorWebChromeClient : WebChromeClient, IActivityResultCallback { // This class implements a permission requesting workflow that matches workflow recommended // by the official Android developer documentation. // See: https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions // The current implementation supports location, camera, and microphone permissions. To add your own, // update the s_rationalesByPermission dictionary to include your rationale for requiring the permission. // If necessary, you may need to also update s_requiredPermissionsByWebkitResource to define how a specific // Webkit resource maps to an Android permission. // In a real app, you would probably use more convincing rationales tailored toward what your app does. private const string CameraAccessRationale = "This app requires access to your camera. Please grant access to your camera when requested."; //private const string LocationAccessRationale = "This app requires access to your location. Please grant access to your precise location when requested."; //private const string MicrophoneAccessRationale = "This app requires access to your microphone. Please grant access to your microphone when requested."; private static readonly Dictionary<string, string> s_rationalesByPermission = new() { [Manifest.Permission.Camera] = CameraAccessRationale, //[Manifest.Permission.AccessFineLocation] = LocationAccessRationale, //[Manifest.Permission.RecordAudio] = MicrophoneAccessRationale, // Add more rationales as you add more supported permissions. }; private static readonly Dictionary<string, string[]> s_requiredPermissionsByWebkitResource = new() { [PermissionRequest.ResourceVideoCapture] = new[] { Manifest.Permission.Camera }, //[PermissionRequest.ResourceAudioCapture] = new[] { Manifest.Permission.ModifyAudioSettings, Manifest.Permission.RecordAudio }, // Add more Webkit resource -> Android permission mappings as needed. }; private readonly WebChromeClient _blazorWebChromeClient; private readonly ComponentActivity _activity; private readonly ActivityResultLauncher _requestPermissionLauncher; private Action<bool>? _pendingPermissionRequestCallback; public PermissionManagingBlazorWebChromeClient(WebChromeClient blazorWebChromeClient, ComponentActivity activity) { _blazorWebChromeClient = blazorWebChromeClient; _activity = activity; _requestPermissionLauncher = _activity.RegisterForActivityResult(new ActivityResultContracts.RequestPermission(), this); } public override void OnCloseWindow(global::Android.Webkit.WebView? window) { _blazorWebChromeClient.OnCloseWindow(window); _requestPermissionLauncher.Unregister(); } public override void OnGeolocationPermissionsShowPrompt(string? origin, GeolocationPermissions.ICallback? callback) { ArgumentNullException.ThrowIfNull(callback, nameof(callback)); RequestPermission(Manifest.Permission.AccessFineLocation, isGranted => callback.Invoke(origin, isGranted, false)); } public override void OnPermissionRequest(PermissionRequest? request) { ArgumentNullException.ThrowIfNull(request, nameof(request)); if (request.GetResources() is not { } requestedResources) { request.Deny(); return; } RequestAllResources(requestedResources, grantedResources => { if (grantedResources.Count == 0) { request.Deny(); } else { request.Grant(grantedResources.ToArray()); } }); } private void RequestAllResources(Memory<string> requestedResources, Action<List<string>> callback) { if (requestedResources.Length == 0) { // No resources to request - invoke the callback with an empty list. callback(new()); return; } var currentResource = requestedResources.Span[0]; var requiredPermissions = s_requiredPermissionsByWebkitResource.GetValueOrDefault(currentResource, Array.Empty<string>()); RequestAllPermissions(requiredPermissions, isGranted => { // Recurse with the remaining resources. If the first resource was granted, use a modified callback // that adds the first resource to the granted resources list. RequestAllResources(requestedResources[1..], !isGranted ? callback : grantedResources => { grantedResources.Add(currentResource); callback(grantedResources); }); }); } private void RequestAllPermissions(Memory<string> requiredPermissions, Action<bool> callback) { if (requiredPermissions.Length == 0) { // No permissions left to request - success! callback(true); return; } RequestPermission(requiredPermissions.Span[0], isGranted => { if (isGranted) { // Recurse with the remaining permissions. RequestAllPermissions(requiredPermissions[1..], callback); } else { // The first required permission was not granted. Fail now and don't attempt to grant // the remaining permissions. callback(false); } }); } private void RequestPermission(string permission, Action<bool> callback) { // This method implements the workflow described here: // https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions if (ContextCompat.CheckSelfPermission(_activity, permission) == Permission.Granted) { callback.Invoke(true); } else if (_activity.ShouldShowRequestPermissionRationale(permission) && s_rationalesByPermission.TryGetValue(permission, out var rationale)) { new AlertDialog.Builder(_activity) .SetTitle("Enable app permissions")! .SetMessage(rationale)! .SetNegativeButton("No thanks", (_, _) => callback(false))! .SetPositiveButton("Continue", (_, _) => LaunchPermissionRequestActivity(permission, callback))! .Show(); } else { LaunchPermissionRequestActivity(permission, callback); } } private void LaunchPermissionRequestActivity(string permission, Action<bool> callback) { if (_pendingPermissionRequestCallback is not null) { throw new InvalidOperationException("Cannot perform multiple permission requests simultaneously."); } _pendingPermissionRequestCallback = callback; _requestPermissionLauncher.Launch(permission); } void IActivityResultCallback.OnActivityResult(Java.Lang.Object isGranted) { var callback = _pendingPermissionRequestCallback; _pendingPermissionRequestCallback = null; callback?.Invoke((bool)isGranted); } #region Unremarkable overrides // See: https://github.com/dotnet/maui/issues/6565 public override JniPeerMembers JniPeerMembers => _blazorWebChromeClient.JniPeerMembers; public override Bitmap? DefaultVideoPoster => _blazorWebChromeClient.DefaultVideoPoster; public override global::Android.Views.View? VideoLoadingProgressView => _blazorWebChromeClient.VideoLoadingProgressView; public override void GetVisitedHistory(IValueCallback? callback) => _blazorWebChromeClient.GetVisitedHistory(callback); public override bool OnConsoleMessage(ConsoleMessage? consoleMessage) => _blazorWebChromeClient.OnConsoleMessage(consoleMessage); public override bool OnCreateWindow(WebView? view, bool isDialog, bool isUserGesture, Message? resultMsg) => _blazorWebChromeClient.OnCreateWindow(view, isDialog, isUserGesture, resultMsg); public override void OnGeolocationPermissionsHidePrompt() => _blazorWebChromeClient.OnGeolocationPermissionsHidePrompt(); public override void OnHideCustomView() => _blazorWebChromeClient.OnHideCustomView(); public override bool OnJsAlert(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsAlert(view, url, message, result); public override bool OnJsBeforeUnload(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsBeforeUnload(view, url, message, result); public override bool OnJsConfirm(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsConfirm(view, url, message, result); public override bool OnJsPrompt(WebView? view, string? url, string? message, string? defaultValue, JsPromptResult? result) => _blazorWebChromeClient.OnJsPrompt(view, url, message, defaultValue, result); public override void OnPermissionRequestCanceled(PermissionRequest? request) => _blazorWebChromeClient.OnPermissionRequestCanceled(request); public override void OnProgressChanged(WebView? view, int newProgress) => _blazorWebChromeClient.OnProgressChanged(view, newProgress); public override void OnReceivedIcon(WebView? view, Bitmap? icon) => _blazorWebChromeClient.OnReceivedIcon(view, icon); public override void OnReceivedTitle(WebView? view, string? title) => _blazorWebChromeClient.OnReceivedTitle(view, title); public override void OnReceivedTouchIconUrl(WebView? view, string? url, bool precomposed) => _blazorWebChromeClient.OnReceivedTouchIconUrl(view, url, precomposed); public override void OnRequestFocus(WebView? view) => _blazorWebChromeClient.OnRequestFocus(view); public override void OnShowCustomView(View? view, ICustomViewCallback? callback) => _blazorWebChromeClient.OnShowCustomView(view, callback); public override bool OnShowFileChooser(WebView? webView, IValueCallback? filePathCallback, FileChooserParams? fileChooserParams) => _blazorWebChromeClient.OnShowFileChooser(webView, filePathCallback, fileChooserParams); #endregion }
还要在MainPage中订阅相关事件
D:\Software\gitee\mauiblazorapp\MaBlaApp\MainPage.xaml.cs
public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized; blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing; } #region 浏览器权限 private void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e) { #if ANDROID if (e.WebView.Context?.GetActivity() is not ComponentActivity activity) { throw new InvalidOperationException($"The permission-managing WebChromeClient requires that the current activity be a '{nameof(ComponentActivity)}'."); } e.WebView.Settings.JavaScriptEnabled = true; e.WebView.Settings.AllowFileAccess = true; e.WebView.Settings.MediaPlaybackRequiresUserGesture = false; e.WebView.Settings.SetGeolocationEnabled(true); e.WebView.Settings.SetGeolocationDatabasePath(e.WebView.Context?.FilesDir?.Path); e.WebView.SetWebChromeClient(new PermissionManagingBlazorWebChromeClient(e.WebView.WebChromeClient!, activity)); #endif } private void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e) { } #endregion }
扫描二维码
新建扫描二维码页面。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\ScanQrCode.razor
@page "/scanqrcode" @using ZXingBlazor.Components @using System.Diagnostics <h3>扫描二维码</h3> <ul class="list-group"> <li class="list-group-item"> <button class="btn btn-primary" type="button" @onclick="OpenBarcodeReader"> <span class="oi oi-vertical-align-center"></span> </button> </li> </ul> @if (ShowScanBarcode) { <BarcodeReader ScanResult="BarcodeReaderOnScanResult" Close="BarcodeReaderOnClose" OnError="BarcodeReaderOnError" ScanBtnTitle="Scan" ResetBtnTitle="Reset" CloseBtnTitle="Close" SelectDeviceBtnTitle="Select Camera" /> } <div class="m-2"> <p>扫描结果:</p> <p>@Value</p> </div> @code { private string Value = ""; //显示扫码界面 private bool ShowScanBarcode = false; private void OpenBarcodeReader() { Value = ""; ShowScanBarcode = true; } private void BarcodeReaderOnScanResult(string result) { Debug.WriteLine($"扫码结果: {result}"); Value = result; ShowScanBarcode = false; } private void BarcodeReaderOnClose() { ShowScanBarcode = false; } private Task BarcodeReaderOnError(string message) { Debug.WriteLine($"扫码错误: {message}"); return Task.CompletedTask; } }
测试运行
在安卓手机(Android 12)上运行,可以扫描二维码,速度很快。
DEMO代码地址:https://gitee.com/woodsun/mauiblazorapp