MAUI Blazor学习6-扫描二维码

MAUI Blazor学习6-扫描二维码

 MAUI Blazor系列目录

  1. MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
  2. MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  3. MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  4. MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  5. 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

 

posted on 2023-01-31 20:42  SunnyTrudeau  阅读(1570)  评论(2编辑  收藏  举报